|
|
@@ -68,6 +68,7 @@ void printUsage() {
|
|
|
<< " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
|
|
|
<< " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
|
|
|
<< " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
|
|
|
+ << " " << C_DIM << "[--limit N] [--offset N] [--exists FIELD] [--eq FIELD VALUE]" << C_RESET << "\n"
|
|
|
<< " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
|
|
|
<< " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
|
|
|
<< " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
|
|
|
@@ -81,13 +82,15 @@ void printUsage() {
|
|
|
<< " list secondary indexes\n"
|
|
|
<< " " << C_CYAN << "index-create" << C_RESET << " <collection> <field>"
|
|
|
<< " declare an index and backfill it\n"
|
|
|
+ << " " << C_DIM << "[--unique] also enforce uniqueness; refused, with examples, if duplicates exist"
|
|
|
+ << C_RESET << "\n"
|
|
|
<< " " << C_CYAN << "index-drop" << C_RESET << " <collection> <field>"
|
|
|
<< " remove an index\n"
|
|
|
<< " " << C_CYAN << "index-values" << C_RESET << " <coll> <field> [n] [asc|desc]"
|
|
|
<< " distinct values + counts\n"
|
|
|
<< C_BOLD << " Relations / referential integrity (v2.11.0+)" << C_RESET << "\n"
|
|
|
<< " " << C_CYAN << "relations" << C_RESET
|
|
|
- << " List declared relations\n"
|
|
|
+ << " List declared relations (every project; --project narrows)\n"
|
|
|
<< " " << C_CYAN << "relation" << C_RESET << " <name>"
|
|
|
<< " Show one relation's declaration\n"
|
|
|
<< " " << C_CYAN << "relation-create" << C_RESET
|
|
|
@@ -115,7 +118,8 @@ void printUsage() {
|
|
|
<< " " << C_DIM << "[--project NAME] [--apply] STOP THE SERVICE and back up before --apply" << C_RESET << "\n\n"
|
|
|
<< C_BOLD << "Options:" << C_RESET << "\n"
|
|
|
<< " --address HOST:PORT Database address (default: localhost:9004)\n"
|
|
|
- << " --project NAME Operate as this project namespace (default: default)\n";
|
|
|
+ << " --project NAME Operate as this project namespace (default: default)\n"
|
|
|
+ << " --version Print version and build commit, then exit\n";
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
@@ -485,7 +489,10 @@ int cmdGenerateTlsCert(const std::vector<std::string>& params) {
|
|
|
}
|
|
|
|
|
|
bool execCommand(smartbotic::database::Client& client,
|
|
|
- const std::string& cmd, const std::vector<std::string>& params) {
|
|
|
+ const std::string& cmd, const std::vector<std::string>& params,
|
|
|
+ // v2.11.1 — empty when the operator passed no --project, which
|
|
|
+ // for `relations` means "every project" rather than "default".
|
|
|
+ const std::string& explicitProject) {
|
|
|
try {
|
|
|
if (cmd == "collections") {
|
|
|
auto collections = client.listCollections();
|
|
|
@@ -515,12 +522,34 @@ bool execCommand(smartbotic::database::Client& client,
|
|
|
}
|
|
|
|
|
|
if (cmd == "find") {
|
|
|
- if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
|
|
|
+ if (params.empty()) {
|
|
|
+ printError("usage: find <collection> [--limit N] [--offset N] [--exists FIELD] [--eq FIELD VALUE]");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
smartbotic::database::Client::QueryOptions opts;
|
|
|
opts.limit = 100;
|
|
|
for (size_t i = 1; i < params.size(); ++i) {
|
|
|
if (params[i] == "--limit" && i + 1 < params.size()) {
|
|
|
opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
|
|
|
+ } else if (params[i] == "--offset" && i + 1 < params.size()) {
|
|
|
+ // v2.11.1 — paging from the CLI. Without it, walking a
|
|
|
+ // collection larger than one page meant deleting as you go,
|
|
|
+ // which is not available when you only want to read.
|
|
|
+ opts.offset = static_cast<uint32_t>(std::stoul(params[++i]));
|
|
|
+ } else if (params[i] == "--eq" && i + 2 < params.size()) {
|
|
|
+ // v2.11.1 — the commonest filter, so cleanup and inspection
|
|
|
+ // do not need a client program. The value is taken as JSON
|
|
|
+ // when it parses as JSON and as a string otherwise, so
|
|
|
+ // --eq status completed and --eq attempts 3 both work.
|
|
|
+ const std::string field = params[++i];
|
|
|
+ const std::string raw = params[++i];
|
|
|
+ nlohmann::json val;
|
|
|
+ try {
|
|
|
+ val = nlohmann::json::parse(raw);
|
|
|
+ } catch (const std::exception&) {
|
|
|
+ val = raw;
|
|
|
+ }
|
|
|
+ opts.filters.emplace_back(field, smartbotic::database::Client::FilterOp::EQ, val);
|
|
|
} else if (params[i] == "--exists" && i + 1 < params.size()) {
|
|
|
opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
|
|
|
}
|
|
|
@@ -710,9 +739,18 @@ bool execCommand(smartbotic::database::Client& client,
|
|
|
// relation names another collection's schema, which is not ordinary
|
|
|
// per-collection write access. Follows the `indexes` output shape.
|
|
|
if (cmd == "relations") {
|
|
|
- auto list = client.listRelations();
|
|
|
+ // v2.11.1 — an operator asking "what is enforced on this database"
|
|
|
+ // means the whole database. Scoping this to the client's own project
|
|
|
+ // made it print "no relations declared" on an instance with active
|
|
|
+ // enforcement in another project, while `relation <name>` (never
|
|
|
+ // project-scoped) happily showed that same relation. --project
|
|
|
+ // narrows it back down.
|
|
|
+ auto list = client.listRelations(explicitProject);
|
|
|
if (list.empty()) {
|
|
|
- std::cout << "no relations declared\n";
|
|
|
+ std::cout << (explicitProject.empty()
|
|
|
+ ? "no relations declared in any project\n"
|
|
|
+ : "no relations declared in project '" +
|
|
|
+ explicitProject + "'\n");
|
|
|
return true;
|
|
|
}
|
|
|
std::cout << C_BOLD << "name child.field -> parent on_delete enforced-at-write"
|
|
|
@@ -1206,6 +1244,20 @@ int main(int argc, char* argv[]) {
|
|
|
} else if (arg == "--help" || arg == "-h") {
|
|
|
printUsage();
|
|
|
return 0;
|
|
|
+ } else if (arg == "--version" || arg == "-V") {
|
|
|
+ // Offline, and before anything else: a version check must not need a
|
|
|
+ // reachable server. Same format as the service binary so the two can
|
|
|
+ // be compared at a glance.
|
|
|
+ std::cout << "smartbotic-db-cli version " << SMARTBOTIC_DB_VERSION_STRING
|
|
|
+ << " (commit " << SMARTBOTIC_DB_GIT_COMMIT_STRING << ")\n"
|
|
|
+ // The LOADED client library, which can differ from the
|
|
|
+ // binary's own version if only one package was upgraded.
|
|
|
+ // That divergence is exactly what an operator is trying to
|
|
|
+ // diagnose when they ask a binary for its version.
|
|
|
+ << "libsmartbotic-db-client "
|
|
|
+ << smartbotic::database::clientLibraryVersion()
|
|
|
+ << " (commit " << smartbotic::database::clientLibraryCommit() << ")\n";
|
|
|
+ return 0;
|
|
|
} else {
|
|
|
positional.push_back(arg);
|
|
|
}
|
|
|
@@ -1236,7 +1288,7 @@ int main(int argc, char* argv[]) {
|
|
|
|
|
|
// Scriptable mode: single command
|
|
|
if (!args.command.empty()) {
|
|
|
- return execCommand(client, args.command, args.params) ? 0 : 1;
|
|
|
+ return execCommand(client, args.command, args.params, args.project) ? 0 : 1;
|
|
|
}
|
|
|
|
|
|
// Interactive mode
|
|
|
@@ -1262,7 +1314,7 @@ int main(int argc, char* argv[]) {
|
|
|
|
|
|
auto cmd = tokens[0];
|
|
|
std::vector<std::string> params(tokens.begin() + 1, tokens.end());
|
|
|
- execCommand(client, cmd, params);
|
|
|
+ execCommand(client, cmd, params, args.project);
|
|
|
}
|
|
|
|
|
|
return 0;
|