use super::args::{self, Args};
use super::scan;
use super::table::Spec;
use crate::reply::Out;
use yo_common::{Code, Error, Result, glob_matches};
use yo_kv::{Applied, Ask, Cond, Keyspace, Kind, MAX_AT, Moved};
const SECOND: i64 = 1000;
const DB_OUT_OF_RANGE: &str = "DB index is out of range";
const SAME_OBJECT: &str = "source and destination objects are the same";
const NX_WITH_OTHERS: &str = "NX and XX, GT or LT options at the same time are not compatible";
const GT_WITH_LT: &str = "GT and LT options at the same time are not compatible";
const NOT_LFU: &str = "An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.";
const OBJECT_HELP: &[&str] = &[
"OBJECT <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"ENCODING <key>",
" Return the kind of internal representation used in order to store the value",
" associated with a <key>.",
"FREQ <key>",
" Return the access frequency index of the <key>. The returned integer is",
" proportional to the logarithm of the real access frequency.",
"IDLETIME <key>",
" Return the idle time of the <key>, that is the approximated number of",
" seconds elapsed since the last access to the value.",
"REFCOUNT <key>",
" Return the number of references of the value associated with the key.",
"HELP",
" Print this help.",
];
pub(super) fn execute(
dbs: &mut [Keyspace],
at: usize,
spec: &Spec,
args: Args<'_>,
out: &mut Out,
) -> Result<()> {
if spec.name == "copy" {
return copy(dbs, at, args, out);
}
let db = &mut dbs[at];
match spec.name {
"del" | "unlink" => {
let mut gone = 0i64;
for i in 1..args.len() {
if db.del(args.get(i)) {
gone += 1;
}
}
out.int(gone);
}
"exists" => {
let mut found = 0i64;
for i in 1..args.len() {
if db.exists(args.get(i)) {
found += 1;
}
}
out.int(found);
}
"type" => {
let name = match db.kind_of(args.get(1)) {
Some(k) => k.name().as_bytes(),
None => &b"none"[..],
};
out.simple(name);
}
"touch" => out.int(db.touch((1..args.len()).map(|i| args.get(i))) as i64),
"rename" | "renamenx" => rename(db, spec.name, args, out)?,
"expire" | "pexpire" | "expireat" | "pexpireat" => expire(db, spec.name, args, out)?,
"persist" => out.int(i64::from(db.persist(args.get(1)))),
"ttl" | "pttl" | "expiretime" | "pexpiretime" => ask(db, spec.name, args, out),
"object" => object(db, args, out)?,
"scan" => scan(db, args, out)?,
"keys" => keys(db, args.get(1), out),
"randomkey" => match db.random_key() {
Some(key) => out.bulk(&key),
None => out.nil(),
},
other => unreachable!("keyspace command with no body: {other}"),
}
Ok(())
}
fn scan(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let cursor = scan::parse_cursor(args.get(1))?;
let mut pattern = None;
let mut count = scan::COUNT;
let mut ty = None;
let mut impossible = false;
let mut i = 2;
while i < args.len() {
let rest = args.len() - i;
if args::is(args.get(i), b"match") && rest >= 2 {
pattern = Some(args.get(i + 1));
} else if args::is(args.get(i), b"count") && rest >= 2 {
count = match args.int(i + 1)? {
n if n >= 1 => usize::try_from(n).unwrap_or(usize::MAX),
_ => return Err(args::syntax()),
};
} else if args::is(args.get(i), b"type") && rest >= 2 {
match kind_named(args.get(i + 1)) {
Some(k) => ty = Some(k),
None => impossible = true,
}
} else {
return Err(args::syntax());
}
i += 2;
}
scan::reply(out, |out| {
let mut n = 0;
let next = db.scan(cursor, count, ty, |key| {
if impossible || pattern.is_some_and(|p| !glob_matches(p, key)) {
return;
}
out.bulk(key);
n += 1;
});
Ok((next, n))
})
}
fn keys(db: &mut Keyspace, pattern: &[u8], out: &mut Out) {
let at = out.len();
let mut n = 0;
db.keys(|key| {
if glob_matches(pattern, key) {
out.bulk(key);
n += 1;
}
});
out.close_array(at, n);
}
fn kind_named(arg: &[u8]) -> Option<Kind> {
[
Kind::String,
Kind::Hash,
Kind::Set,
Kind::Zset,
Kind::List,
Kind::Stream,
]
.into_iter()
.find(|kind| args::is(arg, kind.name().as_bytes()))
}
fn rename(db: &mut Keyspace, name: &str, args: Args<'_>, out: &mut Out) -> Result<()> {
let nx = name == "renamenx";
let done = db.rename(args.get(1), args.get(2), nx).found()?;
if nx {
out.int(i64::from(done == Moved::Ok));
} else {
out.ok();
}
Ok(())
}
fn copy(dbs: &mut [Keyspace], at: usize, args: Args<'_>, out: &mut Out) -> Result<()> {
let (src, dst) = (args.get(1), args.get(2));
let mut into = at;
let mut replace = false;
let mut i = 3;
while i < args.len() {
let arg = args.get(i);
if args::is(arg, b"replace") {
replace = true;
i += 1;
continue;
}
if !args::is(arg, b"db") || i + 1 >= args.len() {
return Err(args::syntax());
}
let n = args.int(i + 1)?;
into = usize::try_from(n)
.ok()
.filter(|n| *n < dbs.len())
.ok_or_else(|| Error::new(Code::Invalid, DB_OUT_OF_RANGE))?;
i += 2;
}
if into == at && src == dst {
return Err(Error::new(Code::Invalid, SAME_OBJECT));
}
let done = if into == at {
dbs[at].copy(src, dst, replace)
} else {
if !replace && dbs[into].exists(dst) {
out.int(0);
return Ok(());
}
let Some(rec) = dbs[at].export(src) else {
out.int(0);
return Ok(());
};
dbs[into].import(dst, rec);
Moved::Ok
};
out.int(i64::from(done == Moved::Ok));
Ok(())
}
fn expire(db: &mut Keyspace, name: &str, args: Args<'_>, out: &mut Out) -> Result<()> {
let relative = matches!(name, "expire" | "pexpire");
let scale = if matches!(name, "pexpire" | "pexpireat") {
1
} else {
SECOND
};
let at = moment(args.int(2)?, scale, relative, name, db.clock().now_ms())?;
let cond = condition(args)?;
out.int(match db.expire(args.get(1), at, cond) {
Applied::Missing | Applied::NotMet => 0,
Applied::Ok | Applied::Deleted => 1,
});
Ok(())
}
fn moment(by: i64, scale: i64, relative: bool, name: &str, now: u64) -> Result<u64> {
let ms = by
.checked_mul(scale)
.and_then(|ms| {
if relative {
ms.checked_add(now as i64)
} else {
Some(ms)
}
})
.ok_or_else(|| {
Error::new(
Code::Invalid,
format!("invalid expire time in '{name}' command"),
)
})?;
Ok(ms.clamp(0, MAX_AT as i64) as u64)
}
fn condition(args: Args<'_>) -> Result<Cond> {
let (mut nx, mut xx, mut gt, mut lt) = (false, false, false, false);
for i in 3..args.len() {
let arg = args.get(i);
match arg {
a if args::is(a, b"nx") => nx = true,
a if args::is(a, b"xx") => xx = true,
a if args::is(a, b"gt") => gt = true,
a if args::is(a, b"lt") => lt = true,
_ => {
return Err(Error::new(
Code::Invalid,
format!("Unsupported option {}", String::from_utf8_lossy(arg)),
));
}
}
}
if nx && (xx || gt || lt) {
return Err(Error::new(Code::Invalid, NX_WITH_OTHERS));
}
if gt && lt {
return Err(Error::new(Code::Invalid, GT_WITH_LT));
}
Ok(match (nx, xx, gt, lt) {
(true, ..) => Cond::NotSet,
(_, _, true, _) => Cond::Greater,
(_, true, _, true) => Cond::LessAndSet,
(_, _, _, true) => Cond::Less,
(_, true, ..) => Cond::AlreadySet,
_ => Cond::Always,
})
}
fn ask(db: &mut Keyspace, name: &str, args: Args<'_>, out: &mut Out) {
let now = db.clock().now_ms();
let asked = db.deadline_of(args.get(1));
let millis = name == "pttl" || name == "pexpiretime";
let ms = if name == "ttl" || name == "pttl" {
asked.remaining_ms(now)
} else {
match asked {
Ask::Missing => -2,
Ask::NoDeadline => -1,
Ask::At(at) => at as i64,
}
};
out.int(if millis || ms < 0 {
ms
} else {
(ms + SECOND / 2) / SECOND
});
}
fn object(db: &mut Keyspace, args: Args<'_>, out: &mut Out) -> Result<()> {
let sub = args.get(1);
if args::is(sub, b"help") {
if args.len() != 2 {
return Err(args::unknown_subcommand(sub, "OBJECT"));
}
out.array(OBJECT_HELP.len());
for line in OBJECT_HELP {
out.simple(line.as_bytes());
}
return Ok(());
}
let named = ["encoding", "refcount", "idletime", "freq"]
.into_iter()
.find(|n| args::is(sub, n.as_bytes()));
let Some(named) = named else {
return Err(args::unknown_subcommand(sub, "OBJECT"));
};
if args.len() != 3 {
return Err(args::wrong_arity_sub("object", named));
}
let key = args.get(2);
if !db.exists(key) {
out.nil();
return Ok(());
}
match named {
"encoding" => {
let name = db
.encoding_name(key)
.expect("the key is there, so it has an encoding");
out.bulk(name.as_bytes());
}
"refcount" => out.int(1),
"idletime" => out.int(0),
"freq" => return Err(Error::new(Code::Unsupported, NOT_LFU)),
other => unreachable!("no body for object {other}"),
}
Ok(())
}