-
Notifications
You must be signed in to change notification settings - Fork 0
feat(search): Add indexes and text search commands #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| use crate::config; | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| #[derive(Deserialize, Serialize)] | ||
| struct Index { | ||
| index_name: String, | ||
| index_type: String, | ||
| columns: Vec<String>, | ||
| metric: Option<String>, | ||
| status: String, | ||
| created_at: String, | ||
| updated_at: String, | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| struct ListResponse { | ||
| indexes: Vec<Index>, | ||
| } | ||
|
|
||
| pub fn list( | ||
| workspace_id: &str, | ||
| connection_id: &str, | ||
| schema: &str, | ||
| table: &str, | ||
| format: &str, | ||
| ) { | ||
| let profile_config = match config::load("default") { | ||
| Ok(c) => c, | ||
| Err(e) => { | ||
| eprintln!("{e}"); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| let api_key = match &profile_config.api_key { | ||
| Some(key) if key != "PLACEHOLDER" => key.clone(), | ||
| _ => { | ||
| eprintln!("error: not authenticated. Run 'hotdata auth' to log in."); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| let url = format!( | ||
| "{}/connections/{}/tables/{}/{}/indexes", | ||
| profile_config.api_url, connection_id, schema, table | ||
| ); | ||
| let client = reqwest::blocking::Client::new(); | ||
|
|
||
| let resp = match client | ||
| .get(&url) | ||
| .header("Authorization", format!("Bearer {api_key}")) | ||
| .header("X-Workspace-Id", workspace_id) | ||
| .send() | ||
| { | ||
| Ok(r) => r, | ||
| Err(e) => { | ||
| eprintln!("error connecting to API: {e}"); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| if !resp.status().is_success() { | ||
| use crossterm::style::Stylize; | ||
| eprintln!("{}", crate::util::api_error(resp.text().unwrap_or_default()).red()); | ||
| std::process::exit(1); | ||
| } | ||
|
|
||
| let body: ListResponse = match resp.json() { | ||
| Ok(v) => v, | ||
| Err(e) => { | ||
| eprintln!("error parsing response: {e}"); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| match format { | ||
| "json" => println!("{}", serde_json::to_string_pretty(&body.indexes).unwrap()), | ||
| "yaml" => print!("{}", serde_yaml::to_string(&body.indexes).unwrap()), | ||
| "table" => { | ||
| if body.indexes.is_empty() { | ||
| use crossterm::style::Stylize; | ||
| eprintln!("{}", "No indexes found.".dark_grey()); | ||
| } else { | ||
| let rows: Vec<Vec<String>> = body.indexes.iter().map(|i| vec![ | ||
| i.index_name.clone(), | ||
| i.index_type.clone(), | ||
| i.columns.join(", "), | ||
| i.metric.clone().unwrap_or_default(), | ||
| i.status.clone(), | ||
| crate::util::format_date(&i.created_at), | ||
| ]).collect(); | ||
| crate::table::print(&["NAME", "TYPE", "COLUMNS", "METRIC", "STATUS", "CREATED"], &rows); | ||
| } | ||
| } | ||
| _ => unreachable!(), | ||
| } | ||
| } | ||
|
|
||
| pub fn create( | ||
| workspace_id: &str, | ||
| connection_id: &str, | ||
| schema: &str, | ||
| table: &str, | ||
| name: &str, | ||
| columns: &str, | ||
| index_type: &str, | ||
| metric: Option<&str>, | ||
| async_mode: bool, | ||
| ) { | ||
| let profile_config = match config::load("default") { | ||
| Ok(c) => c, | ||
| Err(e) => { | ||
| eprintln!("{e}"); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| let api_key = match &profile_config.api_key { | ||
| Some(key) if key != "PLACEHOLDER" => key.clone(), | ||
| _ => { | ||
| eprintln!("error: not authenticated. Run 'hotdata auth' to log in."); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| let cols: Vec<&str> = columns.split(',').map(str::trim).collect(); | ||
| let mut body = serde_json::json!({ | ||
| "index_name": name, | ||
| "columns": cols, | ||
| "index_type": index_type, | ||
| "async": async_mode, | ||
| }); | ||
| if let Some(m) = metric { | ||
| body["metric"] = serde_json::json!(m); | ||
| } | ||
|
|
||
| let url = format!( | ||
| "{}/connections/{}/tables/{}/{}/indexes", | ||
| profile_config.api_url, connection_id, schema, table | ||
| ); | ||
| let client = reqwest::blocking::Client::new(); | ||
|
|
||
| let resp = match client | ||
| .post(&url) | ||
| .header("Authorization", format!("Bearer {api_key}")) | ||
| .header("X-Workspace-Id", workspace_id) | ||
| .json(&body) | ||
| .send() | ||
| { | ||
| Ok(r) => r, | ||
| Err(e) => { | ||
| eprintln!("error connecting to API: {e}"); | ||
| std::process::exit(1); | ||
| } | ||
| }; | ||
|
|
||
| if !resp.status().is_success() { | ||
| use crossterm::style::Stylize; | ||
| eprintln!("{}", crate::util::api_error(resp.text().unwrap_or_default()).red()); | ||
| std::process::exit(1); | ||
| } | ||
|
|
||
| use crossterm::style::Stylize; | ||
| if async_mode { | ||
| let body: serde_json::Value = resp.json().unwrap_or_default(); | ||
| let job_id = body["job_id"].as_str().unwrap_or("unknown"); | ||
| println!("{}", "Index creation submitted.".green()); | ||
| println!("job_id: {}", job_id); | ||
| println!("{}", "Use 'hotdata jobs <job_id>' to check status.".dark_grey()); | ||
| } else { | ||
| println!("{}", "Index created.".green()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ mod config; | |
| mod connections; | ||
| mod connections_new; | ||
| mod datasets; | ||
| mod indexes; | ||
| mod jobs; | ||
| mod query; | ||
| mod results; | ||
|
|
@@ -15,7 +16,7 @@ mod workspace; | |
|
|
||
| use anstyle::AnsiColor; | ||
| use clap::{Parser, builder::Styles}; | ||
| use command::{AuthCommands, Commands, ConnectionsCommands, ConnectionsCreateCommands, DatasetsCommands, JobsCommands, ResultsCommands, SkillCommands, TablesCommands, WorkspaceCommands}; | ||
| use command::{AuthCommands, Commands, ConnectionsCommands, ConnectionsCreateCommands, DatasetsCommands, IndexesCommands, JobsCommands, ResultsCommands, SkillCommands, TablesCommands, WorkspaceCommands}; | ||
|
|
||
| #[derive(Parser)] | ||
| #[command(name = "hotdata", version, about = concat!("Hotdata CLI - Command line interface for Hotdata (v", env!("CARGO_PKG_VERSION"), ")"), long_about = None, disable_version_flag = true)] | ||
|
|
@@ -195,6 +196,33 @@ fn main() { | |
| } | ||
| } | ||
| } | ||
| Commands::Indexes { workspace_id, command } => { | ||
| let workspace_id = resolve_workspace(workspace_id); | ||
| match command { | ||
| IndexesCommands::List { connection_id, schema, table, format } => { | ||
| indexes::list(&workspace_id, &connection_id, &schema, &table, &format) | ||
| } | ||
| IndexesCommands::Create { connection_id, schema, table, name, columns, r#type, metric, r#async } => { | ||
| indexes::create(&workspace_id, &connection_id, &schema, &table, &name, &columns, &r#type, metric.as_deref(), r#async) | ||
| } | ||
| } | ||
| } | ||
| Commands::Search { query, table, column, select, limit, workspace_id, format } => { | ||
| let workspace_id = resolve_workspace(workspace_id); | ||
| let columns = match select.as_deref() { | ||
| Some(cols) => format!("{}, score", cols), | ||
|
Comment on lines
+212
to
+213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The As-is, SELECT 1 FROM other_table --, score FROM bm25_search(...) |
||
| None => "*".to_string(), | ||
| }; | ||
| let sql = format!( | ||
| "SELECT {} FROM bm25_search('{}', '{}', '{}') ORDER BY score DESC LIMIT {}", | ||
| columns, | ||
| table.replace('\'', "''"), | ||
| column.replace('\'', "''"), | ||
| query.replace('\'', "''"), | ||
| limit, | ||
| ); | ||
| query::execute(&sql, &workspace_id, None, &format) | ||
| } | ||
| Commands::Completions { shell } => { | ||
| use clap::CommandFactory; | ||
| use clap_complete::generate; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This collapses two explicit API calls (one for
"pending", one for"running") into a single call with"pending,running". That's cleaner, but it implicitly depends on the API accepting comma-separated status values. If the API doesn't support that syntax it will silently return an empty or wrong result set rather than erroring.Worth either confirming the API contract supports this, or adding a note/test that exercises the combined-status path.