Skip to main content

objectiveai_sdk/cli/command/db/query/
mod.rs

1//! `db query` — execute arbitrary single-statement read-only SQL
2//! against the CLI's local postgres pool. Returns the row set as
3//! typed JSON cells (Postgres → JSON via a per-cell decoder),
4//! with column metadata and the wire-protocol command tag.
5//!
6//! Constraints:
7//! - **Read-only**: wrapped in `SET LOCAL TRANSACTION READ ONLY`
8//!   server-side. Write attempts come back as
9//!   `Error::QueryReadOnlyViolation`.
10//! - **Timeout**: the [`RequestBase`] envelope's optional
11//!   `--timeout` (humantime: `30s`, `5m`, `1h30m`). When set, the
12//!   CLI threads it to postgres as `SET LOCAL statement_timeout` /
13//!   `lock_timeout` — enforcement is the server's alone. When
14//!   omitted, the query runs uncapped.
15//!
16//! [`RequestBase`]: crate::cli::command::RequestBase
17//!
18//! Multi-statement queries, `COPY … TO STDOUT|STDIN`, and
19//! transaction-control verbs (`BEGIN` / `COMMIT` / `ROLLBACK`)
20//! are rejected up front by a cheap leading-token scan on the
21//! CLI side before the query reaches the database.
22
23use crate::cli::command::CommandRequest;
24
25#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
26#[schemars(rename = "cli.command.db.query.Request")]
27pub struct Request {
28    pub path_type: Path,
29    /// SQL statement to execute. Single statement only (multi-
30    /// statement input is rejected by the CLI handler).
31    pub query: String,
32    // Carries the optional `timeout_seconds`; see the module doc.
33    #[serde(flatten)]
34    pub base: crate::cli::command::RequestBase,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
38#[schemars(rename = "cli.command.db.query.Path")]
39pub enum Path {
40    #[serde(rename = "db/query")]
41    DbQuery,
42}
43
44impl CommandRequest for Request {
45    fn into_command(&self) -> Vec<String> {
46        let mut argv = vec![
47            "db".to_string(),
48            "query".to_string(),
49            "--query".to_string(),
50            self.query.clone(),
51        ];
52        self.base.push_flags(&mut argv);
53        argv
54    }
55
56    fn request_base(&self) -> &crate::cli::command::RequestBase {
57        &self.base
58    }
59
60    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
61        Some(&mut self.base)
62    }
63}
64
65/// One result column. `r#type` is the Postgres `pg_type.typname`
66/// (e.g. `"int8"`, `"text"`, `"jsonb"`, `"timestamptz"`). Callers
67/// needing precision/scale/array-element-type can inspect the
68/// name; richer typeinfo is deferred.
69#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
70#[schemars(rename = "cli.command.db.query.Column")]
71pub struct Column {
72    pub name: String,
73    pub r#type: String,
74}
75
76/// Unary response from `db query`.
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
78#[schemars(rename = "cli.command.db.query.Response")]
79pub struct Response {
80    /// Wire-protocol-style command tag — e.g. `"SELECT 5"`,
81    /// `"SHOW 1"`, `"EXPLAIN 12"`, `"SET 0"`. Synthesized from
82    /// `{leading_keyword} {row_count}` since sqlx 0.8 doesn't
83    /// expose the wire-protocol `CommandComplete` tag; close
84    /// enough for telemetry / display.
85    pub command_tag: String,
86    /// Result columns in select-list order. Empty for no-row
87    /// statements (SET / LISTEN / DO).
88    pub columns: Vec<Column>,
89    /// One inner Vec per row, length matches `columns.len()`.
90    /// Each cell is a `serde_json::Value` produced by the CLI's
91    /// per-cell `pg_value_to_json` decoder. Common type
92    /// encodings: text/uuid/timestamps as JSON String, numeric
93    /// as String (preserving precision), bytea as base64 String,
94    /// json/jsonb passthrough as Value, arrays recurse. Empty
95    /// when the statement returns no rows (no-row command OR a
96    /// SELECT with no matches).
97    pub rows: Vec<Vec<serde_json::Value>>,
98    /// Always `false` in the current design. Reserved on the wire
99    /// so a future "soft truncation" mode can be added without a
100    /// shape break.
101    pub truncated: bool,
102}
103
104#[derive(clap::Args)]
105pub struct Args {
106    /// SQL statement to execute. Single statement only.
107    #[arg(long)]
108    pub query: String,
109    #[command(flatten)]
110    pub base: crate::cli::command::RequestBaseArgs,
111}
112
113#[derive(clap::Args)]
114#[command(args_conflicts_with_subcommands = true)]
115pub struct Command {
116    #[command(flatten)]
117    pub args: Args,
118    #[command(subcommand)]
119    pub schema: Option<Schema>,
120}
121
122#[derive(clap::Subcommand)]
123pub enum Schema {
124    /// Emit the JSON Schema for this leaf's `Request` type and exit.
125    RequestSchema(request_schema::Args),
126    /// Emit the JSON Schema for this leaf's `Response` type and exit.
127    ResponseSchema(response_schema::Args),
128}
129
130impl TryFrom<Args> for Request {
131    type Error = crate::cli::command::FromArgsError;
132    fn try_from(args: Args) -> Result<Self, Self::Error> {
133        Ok(Self {
134            path_type: Path::DbQuery,
135            query: args.query,
136            base: args.base.into(),
137        })
138    }
139}
140
141#[cfg(feature = "cli-executor")]
142pub async fn execute<E: crate::cli::command::CommandExecutor>(
143    executor: &E,
144    mut request: Request,
145    agent_arguments: Option<&crate::cli::command::AgentArguments>,
146) -> Result<Response, E::Error> {
147    request.base.clear_transform();
148    executor.execute_one(request, agent_arguments).await
149}
150
151#[cfg(feature = "cli-executor")]
152pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
153    executor: &E,
154    mut request: Request,
155    transform: crate::cli::command::Transform,
156    agent_arguments: Option<&crate::cli::command::AgentArguments>,
157) -> Result<serde_json::Value, E::Error> {
158    request.base.set_transform(transform);
159    executor.execute_one(request, agent_arguments).await
160}
161
162#[cfg(feature = "mcp")]
163impl crate::cli::command::CommandResponse for Response {
164    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
165        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
166    }
167}
168
169pub mod request_schema;
170pub mod response_schema;