Skip to main content

objectiveai_cli/db/logs/
read_id.rs

1//! `agents logs read id` backend: take a `objectiveai.messages."index"`
2//! BIGSERIAL, dispatch to the per-tier target table, and build the
3//! SDK `agents::logs::open::Response` variant with a typed
4//! payload.
5//!
6//! Lookup is two round-trips: one to resolve the message row
7//! (table discriminator + row PK fragments), one to load the
8//! payload from the resolved target table. Each variant assembles
9//! the SDK `Response` directly so the CLI handler stays a thin
10//! pass-through.
11
12use objectiveai_sdk::agent::completions::message::{File, ImageUrl, InputAudio, VideoUrl};
13use objectiveai_sdk::agent::completions::request::AgentCompletionCreateParams;
14use objectiveai_sdk::cli::command::agents::logs::open::Response;
15use objectiveai_sdk::functions::executions::request::FunctionExecutionCreateParams;
16use objectiveai_sdk::vector::completions::request::VectorCompletionCreateParams;
17use sqlx::Row as _;
18
19use super::super::{Error, Pool};
20use super::row::MessageTable;
21
22/// Resolve one `objectiveai.messages."index"` to the matching SDK
23/// `Response` variant. Returns `Ok(None)` when no row exists at
24/// that index.
25pub async fn read_by_id(pool: &Pool, id: i64) -> Result<Option<Response>, Error> {
26    let Some(msg) = sqlx::query(
27        "SELECT response_id, \"table\" AS table_kind, row_index, row_sub_index \
28         FROM objectiveai.messages \
29         WHERE \"index\" = $1",
30    )
31    .bind(id)
32    .fetch_optional(&**pool)
33    .await?
34    else {
35        return Ok(None);
36    };
37
38    // Nullable ONLY for `error` rows (which load by row_index alone).
39    let response_id: Option<String> = msg.try_get("response_id")?;
40    let response_id = response_id.unwrap_or_default();
41    let table_kind: MessageTable = msg.try_get("table_kind")?;
42    let row_index: Option<i64> = msg.try_get("row_index")?;
43    let row_sub_index: Option<i64> = msg.try_get("row_sub_index")?;
44
45    Ok(Some(load_payload(
46        pool,
47        table_kind,
48        &response_id,
49        row_index,
50        row_sub_index,
51    ).await?))
52}
53
54async fn load_payload(
55    pool: &Pool,
56    table_kind: MessageTable,
57    response_id: &str,
58    row_index: Option<i64>,
59    row_sub_index: Option<i64>,
60) -> Result<Response, Error> {
61    match table_kind {
62        // Five message_queue_* variants: `row_index` carries
63        // `message_queue_contents.id`. We dispatch directly to the
64        // matching per-kind table (`message_queue_texts`,
65        // `_images`, etc.) and map onto the same `Text` / `Image`
66        // / `Audio` / `Video` / `File` variants the assistant /
67        // tool content rows use — no separate response shape.
68        MessageTable::MessageQueueText => {
69            let id = require_row_index(table_kind, row_index)?;
70            let text: String = sqlx::query_scalar(
71                "SELECT text FROM objectiveai.message_queue_texts WHERE id = $1",
72            )
73            .bind(id)
74            .fetch_one(&**pool)
75            .await?;
76            Ok(Response::Text { text })
77        }
78        MessageTable::MessageQueueImage => {
79            let id = require_row_index(table_kind, row_index)?;
80            let row = sqlx::query(
81                "SELECT url, detail FROM objectiveai.message_queue_images WHERE id = $1",
82            )
83            .bind(id)
84            .fetch_one(&**pool)
85            .await?;
86            let url: String = row.try_get("url")?;
87            let detail_str: Option<String> = row.try_get("detail")?;
88            let detail = match detail_str {
89                Some(s) => serde_json::from_value(serde_json::Value::String(s))?,
90                None => None,
91            };
92            Ok(Response::Image(ImageUrl { url, detail }))
93        }
94        MessageTable::MessageQueueAudio => {
95            let id = require_row_index(table_kind, row_index)?;
96            let row = sqlx::query(
97                "SELECT data, format FROM objectiveai.message_queue_audios WHERE id = $1",
98            )
99            .bind(id)
100            .fetch_one(&**pool)
101            .await?;
102            Ok(Response::Audio(InputAudio {
103                data: row.try_get("data")?,
104                format: row.try_get("format")?,
105            }))
106        }
107        MessageTable::MessageQueueVideo => {
108            let id = require_row_index(table_kind, row_index)?;
109            let url: String = sqlx::query_scalar(
110                "SELECT url FROM objectiveai.message_queue_videos WHERE id = $1",
111            )
112            .bind(id)
113            .fetch_one(&**pool)
114            .await?;
115            Ok(Response::Video(VideoUrl { url }))
116        }
117        MessageTable::MessageQueueFile => {
118            let id = require_row_index(table_kind, row_index)?;
119            let row = sqlx::query(
120                "SELECT file_data, file_id, filename, file_url \
121                 FROM objectiveai.message_queue_files WHERE id = $1",
122            )
123            .bind(id)
124            .fetch_one(&**pool)
125            .await?;
126            Ok(Response::File(File {
127                file_data: row.try_get("file_data")?,
128                file_id: row.try_get("file_id")?,
129                filename: row.try_get("filename")?,
130                file_url: row.try_get("file_url")?,
131            }))
132        }
133        MessageTable::AgentCompletionRequest => {
134            let (body, created_at, sender) =
135                fetch_request_blob(pool, "objectiveai.agent_completion_requests", response_id).await?;
136            let body: AgentCompletionCreateParams = serde_json::from_value(body)?;
137            Ok(Response::AgentCompletionRequest {
138                response_id: response_id.to_string(),
139                sender_agent_instance_hierarchy: sender,
140                body,
141                created_at,
142            })
143        }
144        MessageTable::VectorCompletionRequest => {
145            let (body, created_at, sender) =
146                fetch_request_blob(pool, "objectiveai.vector_completion_requests", response_id).await?;
147            let body: VectorCompletionCreateParams = serde_json::from_value(body)?;
148            Ok(Response::VectorCompletionRequest {
149                response_id: response_id.to_string(),
150                sender_agent_instance_hierarchy: sender,
151                body,
152                created_at,
153            })
154        }
155        MessageTable::FunctionExecutionRequest => {
156            let (body, created_at, sender) =
157                fetch_request_blob(pool, "objectiveai.function_execution_requests", response_id).await?;
158            let body: FunctionExecutionCreateParams = serde_json::from_value(body)?;
159            Ok(Response::FunctionExecutionRequest {
160                response_id: response_id.to_string(),
161                sender_agent_instance_hierarchy: sender,
162                body,
163                created_at,
164            })
165        }
166        MessageTable::AssistantResponseRefusal => {
167            let index = require_row_index(table_kind, row_index)?;
168            let text = fetch_indexed_text(
169                pool,
170                "objectiveai.assistant_response_refusal",
171                response_id,
172                index,
173            )
174            .await?;
175            Ok(Response::Text { text })
176        }
177        MessageTable::AssistantResponseReasoning => {
178            let index = require_row_index(table_kind, row_index)?;
179            let text = fetch_indexed_text(
180                pool,
181                "objectiveai.assistant_response_reasoning",
182                response_id,
183                index,
184            )
185            .await?;
186            Ok(Response::Text { text })
187        }
188        // A tool-call row reads back as its `arguments` (text). The
189        // call's metadata (function_name / tool_call_id /
190        // tool_call_index) is surfaced inline by `agents logs read all`
191        // on `AssistantResponsePart::ToolCall`, so it isn't repeated
192        // here. `tool_call_index` stays in the WHERE — it's part of the
193        // row PK.
194        MessageTable::AssistantResponseToolCalls => {
195            let index = require_row_index(table_kind, row_index)?;
196            let tool_call_index = require_row_sub_index(table_kind, row_sub_index)?;
197            let row = sqlx::query(
198                "SELECT arguments \
199                 FROM objectiveai.assistant_response_tool_calls \
200                 WHERE response_id = $1 AND \"index\" = $2 AND tool_call_index = $3",
201            )
202            .bind(response_id)
203            .bind(index)
204            .bind(tool_call_index)
205            .fetch_one(&**pool)
206            .await?;
207            Ok(Response::Text { text: row.try_get("arguments")? })
208        }
209        MessageTable::AssistantResponseContentText => {
210            let (index, part_index) =
211                require_full_indices(table_kind, row_index, row_sub_index)?;
212            let text = fetch_content_text(
213                pool,
214                "objectiveai.assistant_response_content_text",
215                response_id,
216                index,
217                part_index,
218            )
219            .await?;
220            Ok(Response::Text { text })
221        }
222        MessageTable::AssistantResponseContentImage => {
223            let (index, part_index) =
224                require_full_indices(table_kind, row_index, row_sub_index)?;
225            let image_url = fetch_content_image(
226                pool,
227                "objectiveai.assistant_response_content_image",
228                response_id,
229                index,
230                part_index,
231            )
232            .await?;
233            Ok(Response::Image(image_url))
234        }
235        MessageTable::AssistantResponseContentAudio => {
236            let (index, part_index) =
237                require_full_indices(table_kind, row_index, row_sub_index)?;
238            let input_audio = fetch_content_audio(
239                pool,
240                "objectiveai.assistant_response_content_audio",
241                response_id,
242                index,
243                part_index,
244            )
245            .await?;
246            Ok(Response::Audio(input_audio))
247        }
248        MessageTable::AssistantResponseContentVideo => {
249            let (index, part_index) =
250                require_full_indices(table_kind, row_index, row_sub_index)?;
251            let video_url = fetch_content_video(
252                pool,
253                "objectiveai.assistant_response_content_video",
254                response_id,
255                index,
256                part_index,
257            )
258            .await?;
259            Ok(Response::Video(video_url))
260        }
261        MessageTable::AssistantResponseContentFile => {
262            let (index, part_index) =
263                require_full_indices(table_kind, row_index, row_sub_index)?;
264            let file = fetch_content_file(
265                pool,
266                "objectiveai.assistant_response_content_file",
267                response_id,
268                index,
269                part_index,
270            )
271            .await?;
272            Ok(Response::File(file))
273        }
274        MessageTable::ToolResponseContentText => {
275            let (index, part_index) =
276                require_full_indices(table_kind, row_index, row_sub_index)?;
277            let text = fetch_content_text(
278                pool,
279                "objectiveai.tool_response_content_text",
280                response_id,
281                index,
282                part_index,
283            )
284            .await?;
285            Ok(Response::Text { text })
286        }
287        MessageTable::ToolResponseContentImage => {
288            let (index, part_index) =
289                require_full_indices(table_kind, row_index, row_sub_index)?;
290            let image_url = fetch_content_image(
291                pool,
292                "objectiveai.tool_response_content_image",
293                response_id,
294                index,
295                part_index,
296            )
297            .await?;
298            Ok(Response::Image(image_url))
299        }
300        MessageTable::ToolResponseContentAudio => {
301            let (index, part_index) =
302                require_full_indices(table_kind, row_index, row_sub_index)?;
303            let input_audio = fetch_content_audio(
304                pool,
305                "objectiveai.tool_response_content_audio",
306                response_id,
307                index,
308                part_index,
309            )
310            .await?;
311            Ok(Response::Audio(input_audio))
312        }
313        MessageTable::ToolResponseContentVideo => {
314            let (index, part_index) =
315                require_full_indices(table_kind, row_index, row_sub_index)?;
316            let video_url = fetch_content_video(
317                pool,
318                "objectiveai.tool_response_content_video",
319                response_id,
320                index,
321                part_index,
322            )
323            .await?;
324            Ok(Response::Video(video_url))
325        }
326        MessageTable::ToolResponseContentFile => {
327            let (index, part_index) =
328                require_full_indices(table_kind, row_index, row_sub_index)?;
329            let file = fetch_content_file(
330                pool,
331                "objectiveai.tool_response_content_file",
332                response_id,
333                index,
334                part_index,
335            )
336            .await?;
337            Ok(Response::File(file))
338        }
339        // ---- request_message user content ----
340        MessageTable::RequestMessageUserContentText => {
341            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
342            Ok(Response::Text { text: fetch_content_text(pool, "objectiveai.request_message_user_content_text", response_id, index, part_index).await? })
343        }
344        MessageTable::RequestMessageUserContentImage => {
345            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
346            Ok(Response::Image(fetch_content_image(pool, "objectiveai.request_message_user_content_image", response_id, index, part_index).await?))
347        }
348        MessageTable::RequestMessageUserContentAudio => {
349            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
350            Ok(Response::Audio(fetch_content_audio(pool, "objectiveai.request_message_user_content_audio", response_id, index, part_index).await?))
351        }
352        MessageTable::RequestMessageUserContentVideo => {
353            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
354            Ok(Response::Video(fetch_content_video(pool, "objectiveai.request_message_user_content_video", response_id, index, part_index).await?))
355        }
356        MessageTable::RequestMessageUserContentFile => {
357            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
358            Ok(Response::File(fetch_content_file(pool, "objectiveai.request_message_user_content_file", response_id, index, part_index).await?))
359        }
360        // ---- request_message assistant ----
361        MessageTable::RequestMessageAssistantRefusal => {
362            let index = require_row_index(table_kind, row_index)?;
363            Ok(Response::Text { text: fetch_indexed_text(pool, "objectiveai.request_message_assistant_refusal", response_id, index).await? })
364        }
365        MessageTable::RequestMessageAssistantReasoning => {
366            let index = require_row_index(table_kind, row_index)?;
367            Ok(Response::Text { text: fetch_indexed_text(pool, "objectiveai.request_message_assistant_reasoning", response_id, index).await? })
368        }
369        MessageTable::RequestMessageAssistantToolCalls => {
370            let index = require_row_index(table_kind, row_index)?;
371            let tool_call_index = require_row_sub_index(table_kind, row_sub_index)?;
372            let row = sqlx::query(
373                "SELECT arguments FROM objectiveai.request_message_assistant_tool_calls \
374                 WHERE response_id = $1 AND \"index\" = $2 AND tool_call_index = $3",
375            )
376            .bind(response_id).bind(index).bind(tool_call_index)
377            .fetch_one(&**pool).await?;
378            Ok(Response::Text { text: row.try_get("arguments")? })
379        }
380        MessageTable::RequestMessageAssistantContentText => {
381            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
382            Ok(Response::Text { text: fetch_content_text(pool, "objectiveai.request_message_assistant_content_text", response_id, index, part_index).await? })
383        }
384        MessageTable::RequestMessageAssistantContentImage => {
385            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
386            Ok(Response::Image(fetch_content_image(pool, "objectiveai.request_message_assistant_content_image", response_id, index, part_index).await?))
387        }
388        MessageTable::RequestMessageAssistantContentAudio => {
389            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
390            Ok(Response::Audio(fetch_content_audio(pool, "objectiveai.request_message_assistant_content_audio", response_id, index, part_index).await?))
391        }
392        MessageTable::RequestMessageAssistantContentVideo => {
393            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
394            Ok(Response::Video(fetch_content_video(pool, "objectiveai.request_message_assistant_content_video", response_id, index, part_index).await?))
395        }
396        MessageTable::RequestMessageAssistantContentFile => {
397            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
398            Ok(Response::File(fetch_content_file(pool, "objectiveai.request_message_assistant_content_file", response_id, index, part_index).await?))
399        }
400        // ---- request_message tool content ----
401        MessageTable::RequestMessageToolContentText => {
402            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
403            Ok(Response::Text { text: fetch_content_text(pool, "objectiveai.request_message_tool_content_text", response_id, index, part_index).await? })
404        }
405        MessageTable::RequestMessageToolContentImage => {
406            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
407            Ok(Response::Image(fetch_content_image(pool, "objectiveai.request_message_tool_content_image", response_id, index, part_index).await?))
408        }
409        MessageTable::RequestMessageToolContentAudio => {
410            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
411            Ok(Response::Audio(fetch_content_audio(pool, "objectiveai.request_message_tool_content_audio", response_id, index, part_index).await?))
412        }
413        MessageTable::RequestMessageToolContentVideo => {
414            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
415            Ok(Response::Video(fetch_content_video(pool, "objectiveai.request_message_tool_content_video", response_id, index, part_index).await?))
416        }
417        MessageTable::RequestMessageToolContentFile => {
418            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
419            Ok(Response::File(fetch_content_file(pool, "objectiveai.request_message_tool_content_file", response_id, index, part_index).await?))
420        }
421        // ---- vector request choice content (index = choice index) ----
422        MessageTable::RequestVectorChoiceContentText => {
423            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
424            Ok(Response::Text { text: fetch_content_text(pool, "objectiveai.request_vector_choice_content_text", response_id, index, part_index).await? })
425        }
426        MessageTable::RequestVectorChoiceContentImage => {
427            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
428            Ok(Response::Image(fetch_content_image(pool, "objectiveai.request_vector_choice_content_image", response_id, index, part_index).await?))
429        }
430        MessageTable::RequestVectorChoiceContentAudio => {
431            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
432            Ok(Response::Audio(fetch_content_audio(pool, "objectiveai.request_vector_choice_content_audio", response_id, index, part_index).await?))
433        }
434        MessageTable::RequestVectorChoiceContentVideo => {
435            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
436            Ok(Response::Video(fetch_content_video(pool, "objectiveai.request_vector_choice_content_video", response_id, index, part_index).await?))
437        }
438        MessageTable::RequestVectorChoiceContentFile => {
439            let (index, part_index) = require_full_indices(table_kind, row_index, row_sub_index)?;
440            Ok(Response::File(fetch_content_file(pool, "objectiveai.request_vector_choice_content_file", response_id, index, part_index).await?))
441        }
442        // The per-agent vote is inline in `read all` (no id). If opened
443        // by id anyway, return its JSON array as text. `response_id` is
444        // the agent completion's id, so there's exactly one vote row.
445        MessageTable::ResponseVectorVote => {
446            let row = sqlx::query(
447                "SELECT vote FROM objectiveai.response_vector_vote \
448                 WHERE response_id = $1 LIMIT 1",
449            )
450            .bind(response_id)
451            .fetch_one(&**pool)
452            .await?;
453            let vote: sqlx::types::Json<serde_json::Value> = row.try_get("vote")?;
454            Ok(Response::Text { text: vote.0.to_string() })
455        }
456        MessageTable::Error => {
457            let id = require_row_index(table_kind, row_index)?;
458            let row = sqlx::query("SELECT error FROM objectiveai.errors WHERE id = $1")
459                .bind(id)
460                .fetch_one(&**pool)
461                .await?;
462            let error: sqlx::types::Json<serde_json::Value> = row.try_get("error")?;
463            Ok(Response::Error { error: error.0 })
464        }
465    }
466}
467
468async fn fetch_request_blob(
469    pool: &Pool,
470    table: &str,
471    response_id: &str,
472) -> Result<(serde_json::Value, i64, String), Error> {
473    let sql = format!(
474        "SELECT body, created_at, sender_agent_instance_hierarchy \
475         FROM {table} WHERE response_id = $1",
476    );
477    let row = sqlx::query(&sql)
478        .bind(response_id)
479        .fetch_one(&**pool)
480        .await?;
481    let body: sqlx::types::Json<serde_json::Value> = row.try_get("body")?;
482    let created_at: i64 = row.try_get("created_at")?;
483    let sender: String = row.try_get("sender_agent_instance_hierarchy")?;
484    Ok((body.0, created_at, sender))
485}
486
487async fn fetch_indexed_text(
488    pool: &Pool,
489    table: &str,
490    response_id: &str,
491    index: i64,
492) -> Result<String, Error> {
493    let sql = format!(
494        "SELECT text FROM {table} \
495         WHERE response_id = $1 AND \"index\" = $2",
496    );
497    let row = sqlx::query(&sql)
498        .bind(response_id)
499        .bind(index)
500        .fetch_one(&**pool)
501        .await?;
502    Ok(row.try_get("text")?)
503}
504
505async fn fetch_content_text(
506    pool: &Pool,
507    table: &str,
508    response_id: &str,
509    index: i64,
510    part_index: i64,
511) -> Result<String, Error> {
512    let sql = format!(
513        "SELECT text FROM {table} \
514         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
515    );
516    let row = sqlx::query(&sql)
517        .bind(response_id)
518        .bind(index)
519        .bind(part_index)
520        .fetch_one(&**pool)
521        .await?;
522    Ok(row.try_get("text")?)
523}
524
525async fn fetch_content_image(
526    pool: &Pool,
527    table: &str,
528    response_id: &str,
529    index: i64,
530    part_index: i64,
531) -> Result<ImageUrl, Error> {
532    let sql = format!(
533        "SELECT url, detail FROM {table} \
534         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
535    );
536    let row = sqlx::query(&sql)
537        .bind(response_id)
538        .bind(index)
539        .bind(part_index)
540        .fetch_one(&**pool)
541        .await?;
542    let url: String = row.try_get("url")?;
543    let detail_str: Option<String> = row.try_get("detail")?;
544    let detail = match detail_str {
545        Some(s) => serde_json::from_value(serde_json::Value::String(s))?,
546        None => None,
547    };
548    Ok(ImageUrl { url, detail })
549}
550
551async fn fetch_content_audio(
552    pool: &Pool,
553    table: &str,
554    response_id: &str,
555    index: i64,
556    part_index: i64,
557) -> Result<InputAudio, Error> {
558    let sql = format!(
559        "SELECT data, format FROM {table} \
560         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
561    );
562    let row = sqlx::query(&sql)
563        .bind(response_id)
564        .bind(index)
565        .bind(part_index)
566        .fetch_one(&**pool)
567        .await?;
568    Ok(InputAudio {
569        data: row.try_get("data")?,
570        format: row.try_get("format")?,
571    })
572}
573
574async fn fetch_content_video(
575    pool: &Pool,
576    table: &str,
577    response_id: &str,
578    index: i64,
579    part_index: i64,
580) -> Result<VideoUrl, Error> {
581    let sql = format!(
582        "SELECT url FROM {table} \
583         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
584    );
585    let row = sqlx::query(&sql)
586        .bind(response_id)
587        .bind(index)
588        .bind(part_index)
589        .fetch_one(&**pool)
590        .await?;
591    Ok(VideoUrl { url: row.try_get("url")? })
592}
593
594async fn fetch_content_file(
595    pool: &Pool,
596    table: &str,
597    response_id: &str,
598    index: i64,
599    part_index: i64,
600) -> Result<File, Error> {
601    let sql = format!(
602        "SELECT file_data, file_id, filename, file_url FROM {table} \
603         WHERE response_id = $1 AND \"index\" = $2 AND part_index = $3",
604    );
605    let row = sqlx::query(&sql)
606        .bind(response_id)
607        .bind(index)
608        .bind(part_index)
609        .fetch_one(&**pool)
610        .await?;
611    Ok(File {
612        file_data: row.try_get("file_data")?,
613        file_id: row.try_get("file_id")?,
614        filename: row.try_get("filename")?,
615        file_url: row.try_get("file_url")?,
616    })
617}
618
619fn require_row_index(
620    table_kind: MessageTable,
621    row_index: Option<i64>,
622) -> Result<i64, Error> {
623    row_index.ok_or_else(|| {
624        Error::InvalidData(format!(
625            "objectiveai.messages row for table {table_kind:?} is missing row_index"
626        ))
627    })
628}
629
630fn require_row_sub_index(
631    table_kind: MessageTable,
632    row_sub_index: Option<i64>,
633) -> Result<i64, Error> {
634    row_sub_index.ok_or_else(|| {
635        Error::InvalidData(format!(
636            "objectiveai.messages row for table {table_kind:?} is missing row_sub_index"
637        ))
638    })
639}
640
641fn require_full_indices(
642    table_kind: MessageTable,
643    row_index: Option<i64>,
644    row_sub_index: Option<i64>,
645) -> Result<(i64, i64), Error> {
646    Ok((
647        require_row_index(table_kind, row_index)?,
648        require_row_sub_index(table_kind, row_sub_index)?,
649    ))
650}