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 request_base(&self) -> &crate::cli::command::RequestBase {
46        &self.base
47    }
48
49    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
50        Some(&mut self.base)
51    }
52}
53
54/// One result column. `r#type` is the Postgres `pg_type.typname`
55/// (e.g. `"int8"`, `"text"`, `"jsonb"`, `"timestamptz"`). Callers
56/// needing precision/scale/array-element-type can inspect the
57/// name; richer typeinfo is deferred.
58#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
59#[schemars(rename = "cli.command.db.query.Column")]
60pub struct Column {
61    pub name: String,
62    pub r#type: String,
63}
64
65/// Unary response from `db query`.
66#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
67#[schemars(rename = "cli.command.db.query.Response")]
68pub struct Response {
69    /// Wire-protocol-style command tag — e.g. `"SELECT 5"`,
70    /// `"SHOW 1"`, `"EXPLAIN 12"`, `"SET 0"`. Synthesized from
71    /// `{leading_keyword} {row_count}` since sqlx 0.8 doesn't
72    /// expose the wire-protocol `CommandComplete` tag; close
73    /// enough for telemetry / display.
74    pub command_tag: String,
75    /// Result columns in select-list order. Empty for no-row
76    /// statements (SET / LISTEN / DO).
77    pub columns: Vec<Column>,
78    /// One inner Vec per row, length matches `columns.len()`.
79    /// Each cell is a `serde_json::Value` produced by the CLI's
80    /// per-cell `pg_value_to_json` decoder. Common type
81    /// encodings: text/uuid/timestamps as JSON String, numeric
82    /// as String (preserving precision), bytea as base64 String,
83    /// json/jsonb passthrough as Value, arrays recurse. Empty
84    /// when the statement returns no rows (no-row command OR a
85    /// SELECT with no matches).
86    pub rows: Vec<Vec<serde_json::Value>>,
87    /// Always `false` in the current design. Reserved on the wire
88    /// so a future "soft truncation" mode can be added without a
89    /// shape break.
90    pub truncated: bool,
91}
92
93#[derive(clap::Args)]
94#[command(group(clap::ArgGroup::new("query_required").required(true).args(["query"])))]
95pub struct Args {
96    /// SQL statement to execute. Single statement only.
97    #[arg(long)]
98    pub query: Option<String>,
99    #[command(flatten)]
100    pub base: crate::cli::command::RequestBaseArgs,
101}
102
103#[derive(clap::Args)]
104#[command(args_conflicts_with_subcommands = true)]
105pub struct Command {
106    #[command(flatten)]
107    pub args: Args,
108    #[command(subcommand)]
109    pub schema: Option<Schema>,
110}
111
112#[derive(clap::Subcommand)]
113pub enum Schema {
114    /// Emit the JSON Schema for this leaf's `Request` type and exit.
115    RequestSchema(request_schema::Args),
116    /// Emit the JSON Schema for this leaf's `Response` type and exit.
117    ResponseSchema(response_schema::Args),
118}
119
120impl TryFrom<Args> for Request {
121    type Error = crate::cli::command::FromArgsError;
122    fn try_from(args: Args) -> Result<Self, Self::Error> {
123        Ok(Self {
124            path_type: Path::DbQuery,
125            query: args.query.ok_or_else(|| {
126                crate::cli::command::FromArgsError::path_parse(
127                    "query",
128                    "--query is required".to_string(),
129                )
130            })?,
131            base: args.base.into(),
132        })
133    }
134}
135
136#[cfg(feature = "cli-executor")]
137pub async fn execute<E: crate::cli::command::CommandExecutor>(
138    executor: &E,
139    mut request: Request,
140    agent_arguments: Option<&crate::cli::command::AgentArguments>,
141) -> Result<Response, E::Error> {
142    request.base.clear_transform();
143    executor.execute_one(request, agent_arguments).await
144}
145
146#[cfg(feature = "cli-executor")]
147pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
148    executor: &E,
149    mut request: Request,
150    transform: crate::cli::command::Transform,
151    agent_arguments: Option<&crate::cli::command::AgentArguments>,
152) -> Result<serde_json::Value, E::Error> {
153    request.base.set_transform(transform);
154    executor.execute_one(request, agent_arguments).await
155}
156
157#[cfg(feature = "mcp")]
158impl crate::cli::command::CommandResponse for Response {
159    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
160        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
161    }
162}
163
164pub mod request_schema;
165pub mod response_schema;