Skip to main content

acorn/io/database/
schema.rs

1//! Database schema model
2//!
3use super::backend::{self, BackendRow, BackendRowExt, Connection, Params};
4use super::{Database, Row, SelectQuery, TableSchemaProvider};
5use crate::io::api::{self, DatabasePersistence};
6use crate::io::database::macros::{build_query, define_required_fn};
7use crate::io::{create_progress_bar, finish_progress_bar, ApiResult, ProgressType};
8use crate::prelude::PathBuf;
9use crate::schema::agent::Weights;
10use crate::util::{print_values_as_table, to_rfc3339, Label};
11use acorn_macros::DatabaseRow;
12use async_trait::async_trait;
13use bon::Builder;
14use color_eyre::eyre::eyre;
15use core::fmt;
16use jiff::Timestamp;
17use serde::{Deserialize, Serialize};
18
19define_required_fn!(1, required1, A, a);
20define_required_fn!(3, required3, A, a, B, b, C, c);
21define_required_fn!(4, required4, A, a, B, b, C, c, D, d);
22define_required_fn!(5, required5, A, a, B, b, C, c, D, d, E, e);
23/// Database tables
24#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[repr(i32)]
26pub enum Table {
27    /// CLI activity log
28    Activity = 0,
29    /// Research activity index entries from buckets
30    Catalog = 1,
31    /// SPDX license data
32    Licenses = 2,
33    /// Cached link check results
34    LinkCache = 3,
35    /// Validation/check results history
36    ValidationHistory = 4,
37    /// Canonical evolving research activity candidates
38    ResearchActivities = 5,
39    /// Programming language metadata cached from GitLab
40    ProgrammingLanguages = 6,
41    /// Models.dev model metadata
42    Models = 7,
43    /// Models.dev provider metadata
44    Providers = 8,
45    /// Persistent identifiers discovered by gather
46    Discoveries = 9,
47}
48/// Activity log entry
49#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
50#[builder(start_fn = init, on(String, into))]
51#[row(table = activity, order_by = "executed_at DESC")]
52pub struct ActivityRow {
53    /// Unique identifier for the activity entry
54    pub id: Option<i64>,
55    /// Command that was executed
56    pub command: Option<String>,
57    /// When the command was executed
58    pub executed_at: Option<Timestamp>,
59    /// Path provided by user (if any)
60    pub user_path: Option<String>,
61    /// Whether the command succeeded
62    pub success: Option<bool>,
63}
64/// Catalog/index entry from bucket
65#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
66#[builder(start_fn = init, on(String, into))]
67#[row(table = catalog, order_by = "title")]
68pub struct CatalogRow {
69    /// Unique identifier for the catalog entry
70    pub id: Option<i64>,
71    /// Name of the bucket
72    pub bucket_name: Option<String>,
73    /// URL of the bucket repository
74    pub bucket_url: Option<String>,
75    /// Research activity identifier
76    pub identifier: Option<String>,
77    /// Research activity title
78    pub title: Option<String>,
79    /// When the entry was last updated
80    pub updated_at: Option<Timestamp>,
81}
82/// Persistent identifier discovery history entry.
83#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
84#[builder(start_fn = init, on(String, into))]
85#[row(table = discoveries, order_by = "discovered_at DESC")]
86pub struct IdentifierRow {
87    /// Unique identifier for the discovery entry.
88    pub id: Option<i64>,
89    /// When the identifier was discovered.
90    pub discovered_at: Option<Timestamp>,
91    /// Normalized persistent identifier.
92    pub identifier: Option<String>,
93    /// Persistent identifier type.
94    pub identifier_type: Option<String>,
95    /// Serialized resolved metadata, when requested and available.
96    pub metadata: Option<String>,
97    /// Resolution outcome.
98    pub resolution_status: Option<String>,
99    /// Input label without raw text content.
100    pub source: Option<String>,
101    /// Detected input format.
102    pub source_format: Option<String>,
103}
104/// SPDX license entry
105#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
106#[builder(start_fn = init, on(String, into))]
107#[row(table = licenses)]
108pub struct LicenseRow {
109    /// Unique identifier for the license entry
110    pub id: Option<i64>,
111    /// Unique license identifier (ex., "MIT")
112    pub identifier: Option<String>,
113    /// License name (ex., "MIT License")
114    pub name: Option<String>,
115    /// Specifies whether the entire license is deprecated
116    pub is_deprecated: Option<bool>,
117    /// Specifies whether the License is listed as free by the Free Software Foundation (FSF)
118    pub is_free_software: Option<bool>,
119    /// Specifies whether the license is approved by the Open Source Initiative (OSI)
120    pub is_open_source: Option<bool>,
121}
122/// Programming language metadata entry
123#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
124#[builder(start_fn = init, on(String, into))]
125#[row(table = programming_languages, order_by = "name")]
126pub struct ProgrammingLanguageRow {
127    /// Unique identifier for the row
128    pub id: Option<i64>,
129    /// Stable language identifier from GitLab linguist metadata
130    pub language_id: Option<i64>,
131    /// Language display name
132    pub name: Option<String>,
133    /// Language category (for example, programming, markup, data, prose)
134    pub language_type: Option<String>,
135    /// Optional color associated with language
136    pub color: Option<String>,
137    /// Optional parent group name
138    pub group_name: Option<String>,
139}
140/// Models.dev model metadata entry
141#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
142#[builder(start_fn = init, on(String, into))]
143#[row(table = models)]
144pub struct ModelRow {
145    /// Unique identifier for the row
146    pub id: Option<i64>,
147    /// Unique model identifier from models.dev
148    pub model_id: Option<String>,
149    /// Model display name
150    pub name: Option<String>,
151    /// Model family (e.g., llama, gemma)
152    pub family: Option<String>,
153    /// Model variant (e.g., coder, instruct)
154    pub variant: Option<String>,
155    /// Model version string
156    pub version: Option<String>,
157    /// Whether the model supports file attachment
158    pub attachment: Option<bool>,
159    /// Whether the model weights are openly available
160    pub open_weights: Option<bool>,
161    /// Whether the model supports extended reasoning
162    pub reasoning: Option<bool>,
163    /// Whether the model supports structured output
164    pub structured_output: Option<bool>,
165    /// Whether the model supports temperature configuration
166    pub temperature: Option<bool>,
167    /// Whether the model supports tool calling
168    pub tool_call: Option<bool>,
169    /// Number of parameters in billions
170    pub parameters: Option<i64>,
171    /// Release date of the model
172    pub release_date: Option<String>,
173    /// Knowledge cutoff date
174    pub knowledge: Option<String>,
175    /// Date the model was last updated
176    pub last_updated: Option<String>,
177    /// Maximum context window size in tokens
178    pub limit_context: Option<i64>,
179    /// Maximum output token count
180    pub limit_output: Option<i64>,
181    /// Maximum input token count
182    pub limit_input: Option<i64>,
183    /// Input modalities supported (JSON array)
184    pub modality_input: Option<String>,
185    /// Output modalities supported (JSON array)
186    pub modality_output: Option<String>,
187    /// Cost per million input tokens
188    pub cost_input: Option<f64>,
189    /// Cost per million output tokens
190    pub cost_output: Option<f64>,
191    /// Cost per million cached input tokens
192    pub cost_cache_read: Option<f64>,
193    /// Cost per million cached write tokens
194    pub cost_cache_write: Option<f64>,
195    /// Extended reasoning cost per million tokens
196    pub cost_reasoning: Option<f64>,
197    /// Cost per million input audio tokens
198    pub cost_input_audio: Option<f64>,
199    /// Cost per million output audio tokens
200    pub cost_output_audio: Option<f64>,
201    /// Pricing for context windows exceeding 200K tokens (JSON)
202    pub cost_over_200k: Option<String>,
203    /// Pricing tiers (JSON array)
204    pub cost_tiers: Option<String>,
205    /// Benchmark evaluation results (JSON array)
206    pub benchmarks: Option<String>,
207    /// Download sources for model weights (JSON array)
208    pub weights: Option<String>,
209}
210/// Models.dev provider metadata entry
211#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
212#[builder(start_fn = init, on(String, into))]
213#[row(table = providers)]
214pub struct ProviderRow {
215    /// Unique identifier for the row
216    pub id: Option<i64>,
217    /// Unique provider identifier from models.dev
218    pub provider_id: Option<String>,
219    /// Provider display name
220    pub name: Option<String>,
221    /// Provider description
222    pub description: Option<String>,
223    /// API endpoint base URL
224    pub endpoint: Option<String>,
225    /// Documentation URL
226    pub documentation: Option<String>,
227    /// Supported authentication methods (JSON array)
228    pub authentication: Option<String>,
229    /// Environment variables required (JSON array)
230    pub env: Option<String>,
231    /// npm package name for the provider's SDK
232    pub npm: Option<String>,
233    /// Provider website URL
234    pub url: Option<String>,
235    /// Date the provider was established
236    pub established_date: Option<String>,
237    /// Date the provider details were last updated
238    pub last_updated: Option<String>,
239    /// Comma-separated list of model IDs offered by this provider
240    pub models: Option<String>,
241}
242/// Link check cache entry
243#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
244#[builder(start_fn = init, on(String, into))]
245#[row(table = link_cache)]
246pub struct LinkCacheRow {
247    /// Unique identifier for the cache entry
248    pub id: Option<i64>,
249    /// The URL that was checked
250    pub url: Option<String>,
251    /// HTTP status code returned
252    pub status_code: Option<i32>,
253    /// Whether the URL was reachable
254    pub is_reachable: Option<bool>,
255    /// When the URL was checked
256    pub checked_at: Option<Timestamp>,
257    /// When the cache entry expires
258    pub expires_at: Option<Timestamp>,
259}
260/// Canonical evolving research activity candidate.
261#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
262#[builder(start_fn = init, on(String, into))]
263#[row(table = research_activities, order_by = "updated_at DESC")]
264pub struct ResearchActivityRow {
265    /// Backend-local numeric identifier.
266    pub id: Option<i64>,
267    /// Portable NanoID identifier.
268    pub iid: Option<String>,
269    /// Canonical partial or complete RAD JSON.
270    pub rad_json: Option<String>,
271    /// Sorted normalized project identity keys as JSON.
272    pub identity_keys_json: Option<String>,
273    /// Deduplicated provenance observations as JSON.
274    pub provenance_json: Option<String>,
275    /// When the candidate was created.
276    pub created_at: Option<Timestamp>,
277    /// When the candidate was last enriched.
278    pub updated_at: Option<Timestamp>,
279}
280/// Validation/check result history
281#[derive(Builder, Clone, DatabaseRow, Debug, Default, Deserialize, Serialize)]
282#[builder(start_fn = init, on(String, into))]
283#[row(table = validation_history, order_by = "checked_at DESC")]
284pub struct ValidationRow {
285    /// Unique identifier for the validation entry
286    pub id: Option<i64>,
287    /// Path to the file that was validated
288    pub path: Option<String>,
289    /// Type of check performed (schema, link, prose, readability)
290    pub check_type: Option<String>,
291    /// Whether the validation passed
292    pub success: Option<bool>,
293    /// Validation message or error details
294    pub message: Option<String>,
295    /// When the validation was performed
296    pub checked_at: Option<Timestamp>,
297}
298impl From<&str> for Table {
299    fn from(value: &str) -> Self {
300        let normalized = value.trim().to_ascii_lowercase();
301        match normalized.as_str() {
302            | "catalog" => Self::Catalog,
303            | "discoveries" | "discovery" => Self::Discoveries,
304            | "licenses" | "license" => Self::Licenses,
305            | "link_cache" | "linkcache" => Self::LinkCache,
306            | "validation_history" | "validationhistory" => Self::ValidationHistory,
307            | "research_activities" | "researchactivities" => Self::ResearchActivities,
308            | "programming_languages" | "programminglanguages" | "languages" | "language" => Self::ProgrammingLanguages,
309            | "models" | "model" => Self::Models,
310            | "providers" | "provider" => Self::Providers,
311            | _ => Self::Activity,
312        }
313    }
314}
315impl From<&BackendRow<'_>> for ModelRow {
316    fn from(row: &BackendRow<'_>) -> Self {
317        ModelRow {
318            id: row.get(0).ok(),
319            model_id: row.get(1).ok(),
320            name: row.get(2).ok(),
321            family: row.get(3).ok(),
322            variant: row.get(4).ok(),
323            version: row.get(5).ok(),
324            attachment: row.get::<_, i32>(6).ok().map(|value| value != 0),
325            open_weights: row.get::<_, i32>(7).ok().map(|value| value != 0),
326            reasoning: row.get::<_, i32>(8).ok().map(|value| value != 0),
327            structured_output: row.get::<_, i32>(9).ok().map(|value| value != 0),
328            temperature: row.get::<_, i32>(10).ok().map(|value| value != 0),
329            tool_call: row.get::<_, i32>(11).ok().map(|value| value != 0),
330            parameters: row.get(12).ok(),
331            release_date: row.get(13).ok(),
332            knowledge: row.get(14).ok(),
333            last_updated: row.get(15).ok(),
334            limit_context: row.get(16).ok(),
335            limit_output: row.get(17).ok(),
336            limit_input: row.get(18).ok(),
337            modality_input: row.get(19).ok(),
338            modality_output: row.get(20).ok(),
339            cost_input: row.get(21).ok(),
340            cost_output: row.get(22).ok(),
341            cost_cache_read: row.get(23).ok(),
342            cost_cache_write: row.get(24).ok(),
343            cost_reasoning: row.get(25).ok(),
344            cost_input_audio: row.get(26).ok(),
345            cost_output_audio: row.get(27).ok(),
346            cost_over_200k: row.get(28).ok(),
347            cost_tiers: row.get(29).ok(),
348            benchmarks: row.get(30).ok(),
349            weights: row.get(31).ok(),
350        }
351    }
352}
353impl From<BackendRow<'_>> for ModelRow {
354    fn from(row: BackendRow<'_>) -> Self {
355        ModelRow::from(&row)
356    }
357}
358impl From<&BackendRow<'_>> for ProviderRow {
359    fn from(row: &BackendRow<'_>) -> Self {
360        ProviderRow {
361            id: row.get(0).ok(),
362            provider_id: row.get(1).ok(),
363            name: row.get(2).ok(),
364            description: row.get(3).ok(),
365            endpoint: row.get(4).ok(),
366            documentation: row.get(5).ok(),
367            authentication: row.get(6).ok(),
368            env: row.get(7).ok(),
369            npm: row.get(8).ok(),
370            url: row.get(9).ok(),
371            established_date: row.get(10).ok(),
372            last_updated: row.get(11).ok(),
373            models: row.get(12).ok(),
374        }
375    }
376}
377impl From<BackendRow<'_>> for ProviderRow {
378    fn from(row: BackendRow<'_>) -> Self {
379        ProviderRow::from(&row)
380    }
381}
382impl From<&BackendRow<'_>> for ActivityRow {
383    fn from(row: &BackendRow<'_>) -> Self {
384        ActivityRow {
385            id: row.get(0).ok(),
386            command: row.get(1).ok(),
387            executed_at: row.parse_rfc3339(2),
388            user_path: row.get(3).ok(),
389            success: row.get::<_, i32>(4).ok().map(|value| value != 0),
390        }
391    }
392}
393impl From<BackendRow<'_>> for ActivityRow {
394    fn from(row: BackendRow<'_>) -> Self {
395        ActivityRow::from(&row)
396    }
397}
398impl From<&BackendRow<'_>> for IdentifierRow {
399    fn from(row: &BackendRow<'_>) -> Self {
400        IdentifierRow {
401            id: row.get(0).ok(),
402            discovered_at: row.parse_rfc3339(1),
403            identifier: row.get(2).ok(),
404            identifier_type: row.get(3).ok(),
405            metadata: row.get(4).ok(),
406            resolution_status: row.get(5).ok(),
407            source: row.get(6).ok(),
408            source_format: row.get(7).ok(),
409        }
410    }
411}
412impl From<BackendRow<'_>> for IdentifierRow {
413    fn from(row: BackendRow<'_>) -> Self {
414        IdentifierRow::from(&row)
415    }
416}
417impl From<&BackendRow<'_>> for LicenseRow {
418    fn from(row: &BackendRow<'_>) -> Self {
419        LicenseRow {
420            id: row.get(0).ok(),
421            identifier: row.get(1).ok(),
422            name: row.get(2).ok(),
423            is_deprecated: row.get::<_, i32>(3).ok().map(|value| value != 0),
424            is_free_software: row.get::<_, i32>(4).ok().map(|value| value != 0),
425            is_open_source: row.get::<_, i32>(5).ok().map(|value| value != 0),
426        }
427    }
428}
429impl From<BackendRow<'_>> for LicenseRow {
430    fn from(row: BackendRow<'_>) -> Self {
431        LicenseRow::from(&row)
432    }
433}
434impl From<&BackendRow<'_>> for ProgrammingLanguageRow {
435    fn from(row: &BackendRow<'_>) -> Self {
436        ProgrammingLanguageRow {
437            id: row.get(0).ok(),
438            language_id: row.get(1).ok(),
439            name: row.get(2).ok(),
440            language_type: row.get(3).ok(),
441            color: row.get(4).ok(),
442            group_name: row.get(5).ok(),
443        }
444    }
445}
446impl From<BackendRow<'_>> for ProgrammingLanguageRow {
447    fn from(row: BackendRow<'_>) -> Self {
448        ProgrammingLanguageRow::from(&row)
449    }
450}
451impl From<&BackendRow<'_>> for CatalogRow {
452    fn from(row: &BackendRow<'_>) -> Self {
453        CatalogRow {
454            id: row.get(0).ok(),
455            bucket_name: row.get(1).ok(),
456            bucket_url: row.get(2).ok(),
457            identifier: row.get(3).ok(),
458            title: row.get(4).ok(),
459            updated_at: row.parse_rfc3339(5),
460        }
461    }
462}
463impl From<BackendRow<'_>> for CatalogRow {
464    fn from(row: BackendRow<'_>) -> Self {
465        CatalogRow::from(&row)
466    }
467}
468impl From<&BackendRow<'_>> for LinkCacheRow {
469    fn from(row: &BackendRow<'_>) -> Self {
470        LinkCacheRow {
471            id: row.get(0).ok(),
472            url: row.get(1).ok(),
473            status_code: row.get(2).ok(),
474            is_reachable: row.get::<_, i32>(3).ok().map(|value| value != 0),
475            checked_at: row.parse_rfc3339(4),
476            expires_at: row.parse_rfc3339(5),
477        }
478    }
479}
480impl From<BackendRow<'_>> for LinkCacheRow {
481    fn from(row: BackendRow<'_>) -> Self {
482        LinkCacheRow::from(&row)
483    }
484}
485impl From<&BackendRow<'_>> for ResearchActivityRow {
486    fn from(row: &BackendRow<'_>) -> Self {
487        ResearchActivityRow {
488            id: row.get(0).ok(),
489            iid: row.get(1).ok(),
490            rad_json: row.get(2).ok(),
491            identity_keys_json: row.get(3).ok(),
492            provenance_json: row.get(4).ok(),
493            created_at: row.parse_rfc3339(5),
494            updated_at: row.parse_rfc3339(6),
495        }
496    }
497}
498impl From<BackendRow<'_>> for ResearchActivityRow {
499    fn from(row: BackendRow<'_>) -> Self {
500        ResearchActivityRow::from(&row)
501    }
502}
503impl From<&BackendRow<'_>> for ValidationRow {
504    fn from(row: &BackendRow<'_>) -> Self {
505        ValidationRow {
506            id: row.get(0).ok(),
507            path: row.get(1).ok(),
508            check_type: row.get(2).ok(),
509            success: row.get::<_, i32>(3).ok().map(|value| value != 0),
510            message: row.get(4).ok(),
511            checked_at: row.parse_rfc3339(5),
512        }
513    }
514}
515impl From<BackendRow<'_>> for ValidationRow {
516    fn from(row: BackendRow<'_>) -> Self {
517        ValidationRow::from(&row)
518    }
519}
520impl From<ActivityRow> for Vec<String> {
521    fn from(row: ActivityRow) -> Self {
522        vec![
523            row.id.map_or_else(String::new, |v| v.to_string()),
524            row.command.unwrap_or_default(),
525            row.executed_at.map_or_else(String::new, to_rfc3339),
526            row.user_path.unwrap_or_default(),
527            row.success.map_or_else(String::new, |v| v.to_string()),
528        ]
529    }
530}
531impl From<CatalogRow> for Vec<String> {
532    fn from(row: CatalogRow) -> Self {
533        vec![
534            row.id.map_or_else(String::new, |v| v.to_string()),
535            row.bucket_name.unwrap_or_default(),
536            row.bucket_url.unwrap_or_default(),
537            row.identifier.unwrap_or_default(),
538            row.title.unwrap_or_default(),
539            row.updated_at.map_or_else(String::new, to_rfc3339),
540        ]
541    }
542}
543impl From<IdentifierRow> for Vec<String> {
544    fn from(row: IdentifierRow) -> Self {
545        vec![
546            row.id.map_or_else(String::new, |value| value.to_string()),
547            row.discovered_at.map_or_else(String::new, to_rfc3339),
548            row.identifier.unwrap_or_default(),
549            row.identifier_type.unwrap_or_default(),
550            row.metadata.unwrap_or_default(),
551            row.resolution_status.unwrap_or_default(),
552            row.source.unwrap_or_default(),
553            row.source_format.unwrap_or_default(),
554        ]
555    }
556}
557impl From<LicenseRow> for Vec<String> {
558    fn from(row: LicenseRow) -> Self {
559        vec![
560            row.id.map_or_else(String::new, |v| v.to_string()),
561            row.identifier.unwrap_or_default(),
562            row.name.unwrap_or_default(),
563            row.is_deprecated.map_or_else(String::new, |v| v.to_string()),
564            row.is_free_software.map_or_else(String::new, |v| v.to_string()),
565            row.is_open_source.map_or_else(String::new, |v| v.to_string()),
566        ]
567    }
568}
569impl From<ProgrammingLanguageRow> for Vec<String> {
570    fn from(row: ProgrammingLanguageRow) -> Self {
571        vec![
572            row.id.map_or_else(String::new, |v| v.to_string()),
573            row.language_id.map_or_else(String::new, |v| v.to_string()),
574            row.name.unwrap_or_default(),
575            row.language_type.unwrap_or_default(),
576            row.color.unwrap_or_default(),
577            row.group_name.unwrap_or_default(),
578        ]
579    }
580}
581impl From<LinkCacheRow> for Vec<String> {
582    fn from(row: LinkCacheRow) -> Self {
583        vec![
584            row.id.map_or_else(String::new, |v| v.to_string()),
585            row.url.unwrap_or_default(),
586            row.status_code.map_or_else(String::new, |v| v.to_string()),
587            row.is_reachable.map_or_else(String::new, |v| v.to_string()),
588            row.checked_at.map_or_else(String::new, to_rfc3339),
589            row.expires_at.map_or_else(String::new, to_rfc3339),
590        ]
591    }
592}
593impl From<ResearchActivityRow> for Vec<String> {
594    fn from(row: ResearchActivityRow) -> Self {
595        vec![
596            row.id.map_or_else(String::new, |v| v.to_string()),
597            row.iid.unwrap_or_default(),
598            row.rad_json.unwrap_or_default(),
599            row.identity_keys_json.unwrap_or_default(),
600            row.provenance_json.unwrap_or_default(),
601            row.created_at.map_or_else(String::new, to_rfc3339),
602            row.updated_at.map_or_else(String::new, to_rfc3339),
603        ]
604    }
605}
606impl From<ValidationRow> for Vec<String> {
607    fn from(row: ValidationRow) -> Self {
608        vec![
609            row.id.map_or_else(String::new, |v| v.to_string()),
610            row.path.unwrap_or_default(),
611            row.check_type.unwrap_or_default(),
612            row.success.map_or_else(String::new, |v| v.to_string()),
613            row.message.unwrap_or_default(),
614            row.checked_at.map_or_else(String::new, to_rfc3339),
615        ]
616    }
617}
618impl From<ModelRow> for Vec<String> {
619    fn from(row: ModelRow) -> Self {
620        vec![
621            row.id.map_or_else(String::new, |v| v.to_string()),
622            row.model_id.unwrap_or_default(),
623            row.name.unwrap_or_default(),
624            row.family.unwrap_or_default(),
625            row.variant.unwrap_or_default(),
626            row.version.unwrap_or_default(),
627            row.attachment.map_or_else(String::new, |v| v.to_string()),
628            row.open_weights.map_or_else(String::new, |v| v.to_string()),
629            row.reasoning.map_or_else(String::new, |v| v.to_string()),
630            row.structured_output.map_or_else(String::new, |v| v.to_string()),
631            row.temperature.map_or_else(String::new, |v| v.to_string()),
632            row.tool_call.map_or_else(String::new, |v| v.to_string()),
633            row.parameters.map_or_else(String::new, |v| v.to_string()),
634            row.release_date.unwrap_or_default(),
635            row.knowledge.unwrap_or_default(),
636            row.last_updated.unwrap_or_default(),
637            row.limit_context.map_or_else(String::new, |v| v.to_string()),
638            row.limit_output.map_or_else(String::new, |v| v.to_string()),
639            row.limit_input.map_or_else(String::new, |v| v.to_string()),
640            row.modality_input.unwrap_or_default(),
641            row.modality_output.unwrap_or_default(),
642            row.cost_input.map_or_else(String::new, |v| v.to_string()),
643            row.cost_output.map_or_else(String::new, |v| v.to_string()),
644            row.cost_cache_read.map_or_else(String::new, |v| v.to_string()),
645            row.cost_cache_write.map_or_else(String::new, |v| v.to_string()),
646            row.cost_reasoning.map_or_else(String::new, |v| v.to_string()),
647            row.cost_input_audio.map_or_else(String::new, |v| v.to_string()),
648            row.cost_output_audio.map_or_else(String::new, |v| v.to_string()),
649            row.cost_over_200k.unwrap_or_default(),
650            row.cost_tiers.unwrap_or_default(),
651            row.benchmarks.unwrap_or_default(),
652            row.weights.unwrap_or_default(),
653        ]
654    }
655}
656impl From<ProviderRow> for Vec<String> {
657    fn from(row: ProviderRow) -> Self {
658        vec![
659            row.id.map_or_else(String::new, |v| v.to_string()),
660            row.provider_id.unwrap_or_default(),
661            row.name.unwrap_or_default(),
662            row.description.unwrap_or_default(),
663            row.endpoint.unwrap_or_default(),
664            row.documentation.unwrap_or_default(),
665            row.authentication.unwrap_or_default(),
666            row.env.unwrap_or_default(),
667            row.npm.unwrap_or_default(),
668            row.url.unwrap_or_default(),
669            row.established_date.unwrap_or_default(),
670            row.last_updated.unwrap_or_default(),
671            row.models.unwrap_or_default(),
672        ]
673    }
674}
675impl fmt::Display for ActivityRow {
676    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
677        write!(
678            f,
679            "ActivityRow = id: {}, command: {}, success: {}",
680            display_or_none(self.id.as_ref()),
681            display_or_none(self.command.as_ref()),
682            self.success.unwrap_or(false),
683        )
684        .and_then(|_| write_optional_field(f, "path", self.user_path.as_ref()))
685        .and_then(|_| write_optional_timestamp(f, "executed_at", self.executed_at.as_ref()))
686    }
687}
688impl fmt::Display for LicenseRow {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        write!(
691            f,
692            "LicenseRow = id: {}, identifier: {}, name: {}, deprecated: {}, free: {}, osi: {}",
693            display_or_none(self.id.as_ref()),
694            display_or_none(self.identifier.as_ref()),
695            display_or_none(self.name.as_ref()),
696            self.is_deprecated.unwrap_or(false),
697            self.is_free_software.unwrap_or(false),
698            self.is_open_source.unwrap_or(false),
699        )
700    }
701}
702impl fmt::Display for CatalogRow {
703    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
704        write!(
705            f,
706            "CatalogRow = id: {}, bucket: {}, identifier: {}, title: {}",
707            display_or_none(self.id.as_ref()),
708            display_or_none(self.bucket_name.as_ref()),
709            display_or_none(self.identifier.as_ref()),
710            display_or_none(self.title.as_ref())
711        )
712        .and_then(|_| write_optional_timestamp(f, "updated_at", self.updated_at.as_ref()))
713    }
714}
715impl fmt::Display for IdentifierRow {
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717        write!(
718            f,
719            "IdentifierRow = id: {}, identifier: {}, type: {}, source: {}",
720            display_or_none(self.id.as_ref()),
721            display_or_none(self.identifier.as_ref()),
722            display_or_none(self.identifier_type.as_ref()),
723            display_or_none(self.source.as_ref())
724        )
725        .and_then(|_| write_optional_timestamp(f, "discovered_at", self.discovered_at.as_ref()))
726    }
727}
728impl fmt::Display for ProgrammingLanguageRow {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        write!(
731            f,
732            "ProgrammingLanguageRow = id: {}, language_id: {}, name: {}, type: {}",
733            display_or_none(self.id.as_ref()),
734            display_or_none(self.language_id.as_ref()),
735            display_or_none(self.name.as_ref()),
736            display_or_none(self.language_type.as_ref())
737        )
738        .and_then(|_| write_optional_field(f, "color", self.color.as_ref()))
739        .and_then(|_| write_optional_field(f, "group_name", self.group_name.as_ref()))
740    }
741}
742impl fmt::Display for LinkCacheRow {
743    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
744        write!(
745            f,
746            "LinkCacheRow = id: {}, url: {}, status: {}, reachable: {}",
747            display_or_none(self.id.as_ref()),
748            display_or_none(self.url.as_ref()),
749            self.status_code.unwrap_or(0),
750            self.is_reachable.unwrap_or(false)
751        )
752        .and_then(|_| write_optional_timestamp(f, "checked_at", self.checked_at.as_ref()))
753        .and_then(|_| write_optional_timestamp(f, "expires_at", self.expires_at.as_ref()))
754    }
755}
756impl fmt::Display for ResearchActivityRow {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        write!(
759            f,
760            "ResearchActivityRow = id: {}, iid: {}",
761            display_or_none(self.id.as_ref()),
762            display_or_none(self.iid.as_ref())
763        )
764        .and_then(|_| write_optional_timestamp(f, "created_at", self.created_at.as_ref()))
765        .and_then(|_| write_optional_timestamp(f, "updated_at", self.updated_at.as_ref()))
766    }
767}
768impl fmt::Display for ValidationRow {
769    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770        write!(
771            f,
772            "ValidationRow = id: {}, path: {}, type: {}, success: {}, message: {}",
773            display_or_none(self.id.as_ref()),
774            display_or_none(self.path.as_ref()),
775            display_or_none(self.check_type.as_ref()),
776            self.success.unwrap_or(false),
777            display_or_none(self.message.as_ref())
778        )
779        .and_then(|_| write_optional_timestamp(f, "checked_at", self.checked_at.as_ref()))
780    }
781}
782impl fmt::Display for ModelRow {
783    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784        write!(
785            f,
786            "ModelRow = id: {}, model_id: {}, name: {}, family: {}",
787            display_or_none(self.id.as_ref()),
788            display_or_none(self.model_id.as_ref()),
789            display_or_none(self.name.as_ref()),
790            display_or_none(self.family.as_ref())
791        )
792    }
793}
794impl ModelRow {
795    /// Deserialize the model's persisted weight metadata.
796    pub fn parsed_weights(&self) -> Option<Weights> {
797        self.weights.as_deref().and_then(|weights| serde_json::from_str(weights).ok())
798    }
799    /// Update the serialized weight metadata for this model.
800    pub fn update_weights(&self, path: Option<PathBuf>) -> ApiResult<usize> {
801        match (self.model_id.as_deref(), self.weights.as_deref()) {
802            | (Some(model_id), Some(weights)) => Database::<Table>::from_path(path).with_connection(|conn| {
803                conn.execute("UPDATE models SET weights = ? WHERE model_id = ?", backend::params![weights, model_id])
804                    .map_err(|why| eyre!("=> {} Failed to update model weights: {why}", Label::fail()))
805            }),
806            | (None, _) => Err(eyre!("Model identifier is required to update model weights")),
807            | (_, None) => Err(eyre!("Model weights are required to update model weights")),
808        }
809    }
810}
811impl fmt::Display for ProviderRow {
812    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813        write!(
814            f,
815            "ProviderRow = id: {}, provider_id: {}, name: {}",
816            display_or_none(self.id.as_ref()),
817            display_or_none(self.provider_id.as_ref()),
818            display_or_none(self.name.as_ref())
819        )
820        .and_then(|_| write_optional_field(f, "description", self.description.as_ref()))
821    }
822}
823#[async_trait]
824impl TableSchemaProvider for Table {
825    fn all() -> &'static [Self] {
826        &[
827            Table::Activity,
828            Table::Catalog,
829            Table::Discoveries,
830            Table::Licenses,
831            Table::LinkCache,
832            Table::ValidationHistory,
833            Table::ResearchActivities,
834            Table::ProgrammingLanguages,
835            Table::Models,
836            Table::Providers,
837        ]
838    }
839    /// Returns CREATE TABLE SQL statement
840    fn create_statement(&self) -> String {
841        let columns = match self {
842            | Table::Activity => {
843                r#"
844                    command TEXT NOT NULL,
845                    executed_at TEXT NOT NULL,
846                    user_path TEXT,
847                    success INTEGER NOT NULL DEFAULT 1
848                "#
849            }
850            | Table::Catalog => {
851                r#"
852                    bucket_name TEXT NOT NULL,
853                    bucket_url TEXT NOT NULL,
854                    identifier TEXT NOT NULL,
855                    title TEXT NOT NULL,
856                    updated_at TEXT NOT NULL,
857                    UNIQUE(bucket_name, identifier)
858                "#
859            }
860            | Table::Discoveries => {
861                r#"
862                    discovered_at TEXT NOT NULL,
863                    identifier TEXT NOT NULL,
864                    identifier_type TEXT NOT NULL,
865                    metadata TEXT,
866                    resolution_status TEXT NOT NULL,
867                    source TEXT NOT NULL,
868                    source_format TEXT NOT NULL
869                "#
870            }
871            | Table::Licenses => {
872                r#"
873                    identifier TEXT NOT NULL UNIQUE,
874                    name TEXT NOT NULL,
875                    is_deprecated INTEGER NOT NULL,
876                    is_free_software INTEGER NOT NULL,
877                    is_open_source INTEGER NOT NULL
878                "#
879            }
880            | Table::LinkCache => {
881                r#"
882                    url TEXT NOT NULL UNIQUE,
883                    status_code INTEGER NOT NULL,
884                    is_reachable INTEGER NOT NULL,
885                    checked_at TEXT NOT NULL,
886                    expires_at TEXT NOT NULL
887                "#
888            }
889            | Table::ValidationHistory => {
890                r#"
891                    path TEXT NOT NULL,
892                    check_type TEXT NOT NULL,
893                    success INTEGER NOT NULL,
894                    message TEXT NOT NULL,
895                    checked_at TEXT NOT NULL
896                "#
897            }
898            | Table::ResearchActivities => {
899                r#"
900                    iid TEXT NOT NULL UNIQUE,
901                    rad_json TEXT NOT NULL,
902                    identity_keys_json TEXT NOT NULL,
903                    provenance_json TEXT NOT NULL,
904                    created_at TEXT NOT NULL,
905                    updated_at TEXT NOT NULL
906                "#
907            }
908            | Table::ProgrammingLanguages => {
909                r#"
910                    language_id INTEGER,
911                    name TEXT NOT NULL UNIQUE,
912                    language_type TEXT,
913                    color TEXT,
914                    group_name TEXT
915                "#
916            }
917            | Table::Models => {
918                r#"
919                    model_id TEXT NOT NULL UNIQUE,
920                    name TEXT,
921                    family TEXT,
922                    variant TEXT,
923                    version TEXT,
924                    attachment INTEGER,
925                    open_weights INTEGER,
926                    reasoning INTEGER,
927                    structured_output INTEGER,
928                    temperature INTEGER,
929                    tool_call INTEGER,
930                    parameters INTEGER,
931                    release_date TEXT,
932                    knowledge TEXT,
933                    last_updated TEXT,
934                    limit_context INTEGER,
935                    limit_output INTEGER,
936                    limit_input INTEGER,
937                    modality_input TEXT,
938                    modality_output TEXT,
939                    cost_input REAL,
940                    cost_output REAL,
941                    cost_cache_read REAL,
942                    cost_cache_write REAL,
943                    cost_reasoning REAL,
944                    cost_input_audio REAL,
945                    cost_output_audio REAL,
946                    cost_over_200k TEXT,
947                    cost_tiers TEXT,
948                    benchmarks TEXT,
949                    weights TEXT
950                "#
951            }
952            | Table::Providers => {
953                r#"
954                    provider_id TEXT NOT NULL UNIQUE,
955                    name TEXT,
956                    description TEXT,
957                    endpoint TEXT,
958                    documentation TEXT,
959                    authentication TEXT,
960                    env TEXT,
961                    npm TEXT,
962                    url TEXT,
963                    established_date TEXT,
964                    last_updated TEXT,
965                    models TEXT
966                "#
967            }
968        };
969
970        format!(
971            r#"
972                CREATE TABLE IF NOT EXISTS {} (
973                    {},
974                    {}
975                )
976                "#,
977            self.name(),
978            id_column_definition(),
979            columns.trim()
980        )
981    }
982    /// Returns the table name as a string
983    fn name(&self) -> &'static str {
984        match self {
985            | Table::Activity => "activity",
986            | Table::Catalog => "catalog",
987            | Table::Discoveries => "discoveries",
988            | Table::Licenses => "licenses",
989            | Table::LinkCache => "link_cache",
990            | Table::ValidationHistory => "validation_history",
991            | Table::ResearchActivities => "research_activities",
992            | Table::ProgrammingLanguages => "programming_languages",
993            | Table::Models => "models",
994            | Table::Providers => "providers",
995        }
996    }
997    async fn populate(&self, path: Option<PathBuf>) -> ApiResult<usize> {
998        match &self {
999            | Table::Licenses => {
1000                let progress = create_progress_bar(0, ProgressType::Spinner);
1001                progress.set_message("Downloading SPDX license data...");
1002                let response = api::spdx::download().await;
1003                let message = format!("{}SPDX metadata download complete", Label::CHECKMARK);
1004                finish_progress_bar(&progress, message);
1005                match response {
1006                    | Ok(data) => data.persist(Database::<Self>::from_path(path.clone())).await,
1007                    | Err(why) => Err(eyre!("Failed to download SPDX license data — {why}")),
1008                }
1009            }
1010            | Table::ProgrammingLanguages => {
1011                let progress = create_progress_bar(0, ProgressType::Spinner);
1012                progress.set_message("Downloading GitLab programming language data...");
1013                let response = api::gitlab::languages().await;
1014                let message = format!("{}GitLab programming language metadata download complete", Label::CHECKMARK);
1015                finish_progress_bar(&progress, message);
1016                match response {
1017                    | Ok(data) => data.persist(Database::<Self>::from_path(path.clone())).await,
1018                    | Err(why) => Err(eyre!("Failed to download GitLab programming language data — {why}")),
1019                }
1020            }
1021            | Table::Models => {
1022                let progress = create_progress_bar(0, ProgressType::Spinner);
1023                progress.set_message("Downloading models.dev catalog (for models)...");
1024                let response = api::models_dev::download_cached().await;
1025                let message = format!("{}Models.dev catalog download complete", Label::CHECKMARK);
1026                finish_progress_bar(&progress, message);
1027                match response {
1028                    | Ok(catalog) => catalog.models().persist(Database::<Self>::from_path(path.clone())).await,
1029                    | Err(why) => Err(eyre!("Failed to download models.dev catalog — {why}")),
1030                }
1031            }
1032            | Table::Providers => {
1033                let progress = create_progress_bar(0, ProgressType::Spinner);
1034                progress.set_message("Downloading models.dev catalog (for providers)...");
1035                let response = api::models_dev::download_cached().await;
1036                let message = format!("{}Models.dev catalog download complete", Label::CHECKMARK);
1037                finish_progress_bar(&progress, message);
1038                match response {
1039                    | Ok(catalog) => catalog.providers().persist(Database::<Self>::from_path(path.clone())).await,
1040                    | Err(why) => Err(eyre!("Failed to download models.dev catalog — {why}")),
1041                }
1042            }
1043            | table => Err(eyre!("populate() not implemented for {} table", table.name())),
1044        }
1045    }
1046    /// Print all table rows
1047    /// ### Example
1048    /// ```ignore
1049    /// use acorn::io::database::{TableSchemaProvider, schema::Table};
1050    ///
1051    /// Table::Activity.print(database_path.clone());
1052    /// ```
1053    fn print(self, path: Option<PathBuf>) {
1054        fn print_rows<R>(table: Table, path: Option<&PathBuf>)
1055        where
1056            R: Into<Vec<String>> + Row + Default + fmt::Display,
1057            for<'row> R: From<&'row BackendRow<'row>>,
1058        {
1059            let row = R::default();
1060            let fields = <R as Row>::fields();
1061            let query = format!("SELECT {} FROM {}", fields.join(", "), table.name());
1062            let (query, params) = row.build_select_query(&query);
1063            let rows = table
1064                .rows::<R, _>(&query, params, path)
1065                .map(|rows| rows.into_iter().map(Into::into).collect::<Vec<Vec<String>>>())
1066                .unwrap_or_default();
1067            print_values_as_table::<String>(fields.to_vec(), rows, Some(table.name().to_string()));
1068        }
1069        match self {
1070            | Table::Activity => print_rows::<ActivityRow>(self, path.as_ref()),
1071            | Table::Catalog => print_rows::<CatalogRow>(self, path.as_ref()),
1072            | Table::Discoveries => print_rows::<IdentifierRow>(self, path.as_ref()),
1073            | Table::Licenses => print_rows::<LicenseRow>(self, path.as_ref()),
1074            | Table::ProgrammingLanguages => print_rows::<ProgrammingLanguageRow>(self, path.as_ref()),
1075            | Table::LinkCache => print_rows::<LinkCacheRow>(self, path.as_ref()),
1076            | Table::ValidationHistory => print_rows::<ValidationRow>(self, path.as_ref()),
1077            | Table::ResearchActivities => print_rows::<ResearchActivityRow>(self, path.as_ref()),
1078            | Table::Models => print_rows::<ModelRow>(self, path.as_ref()),
1079            | Table::Providers => print_rows::<ProviderRow>(self, path.as_ref()),
1080        }
1081    }
1082    /// Get table rows
1083    fn rows<R, P>(&self, query: &str, params: P, path: Option<&PathBuf>) -> ApiResult<Vec<R>>
1084    where
1085        P: Params,
1086        for<'row> R: Row + From<&'row BackendRow<'row>>,
1087    {
1088        Database::<Table>::from_path(path.cloned()).with_connection(|conn| {
1089            conn.prepare(query)
1090                .map_err(|why| eyre!("=> {} Failed to prepare {} query: {why}", Label::fail(), self.name()))
1091                .and_then(|mut stmt| {
1092                    stmt.query_map(params, |row| Ok(R::from(row)))
1093                        .map_err(|why| eyre!("=> {} Failed to query {}: {why}", Label::fail(), self.name()))
1094                        .and_then(|rows| {
1095                            rows.collect::<core::result::Result<Vec<_>, _>>()
1096                                .map_err(|why| eyre!("=> {} Failed to read {} rows: {why}", Label::fail(), self.name()))
1097                        })
1098                })
1099        })
1100    }
1101}
1102impl Row for ActivityRow {
1103    fn table(&self) -> Table {
1104        Table::Activity
1105    }
1106    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1107        let Self {
1108            command,
1109            executed_at,
1110            user_path,
1111            success,
1112            ..
1113        } = self.clone();
1114        let executed_at = to_rfc3339(executed_at.unwrap_or_else(Timestamp::now));
1115        let success = i32::from(success.unwrap_or(true));
1116        let required = required1(command);
1117        match required {
1118            | Ok((command,)) => match self.next_row_id(conn) {
1119                | Ok(id) => {
1120                    let params = backend::params![id, command, executed_at, user_path, success];
1121                    execute_insert(&self, conn, params)
1122                }
1123                | Err(why) => Err(why),
1124            },
1125            | Err(why) => Err(why),
1126        }
1127    }
1128    fn build_select_query(&self, base: &str) -> SelectQuery {
1129        Self::build_select_query(self, base)
1130    }
1131}
1132impl Row for CatalogRow {
1133    fn table(&self) -> Table {
1134        Table::Catalog
1135    }
1136    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1137        let Self {
1138            bucket_name,
1139            bucket_url,
1140            identifier,
1141            title,
1142            updated_at,
1143            ..
1144        } = self.clone();
1145        let updated_at = to_rfc3339(updated_at.unwrap_or_else(Timestamp::now));
1146        let required = required4(bucket_name, bucket_url, identifier, title);
1147        match required {
1148            | Ok((bucket_name, bucket_url, identifier, title)) => match self.next_row_id(conn) {
1149                | Ok(id) => {
1150                    let params = backend::params![id, bucket_name, bucket_url, identifier, title, updated_at];
1151                    execute_insert(&self, conn, params)
1152                }
1153                | Err(why) => Err(why),
1154            },
1155            | Err(why) => Err(why),
1156        }
1157    }
1158    fn build_select_query(&self, base: &str) -> SelectQuery {
1159        Self::build_select_query(self, base)
1160    }
1161}
1162impl Row for IdentifierRow {
1163    fn table(&self) -> Table {
1164        Table::Discoveries
1165    }
1166    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1167        let Self {
1168            discovered_at,
1169            identifier,
1170            identifier_type,
1171            metadata,
1172            resolution_status,
1173            source,
1174            source_format,
1175            ..
1176        } = self.clone();
1177        let discovered_at = to_rfc3339(discovered_at.unwrap_or_else(Timestamp::now));
1178        match required1(identifier) {
1179            | Ok((identifier,)) => match self.next_row_id(conn) {
1180                | Ok(id) => execute_insert(
1181                    &self,
1182                    conn,
1183                    backend::params![
1184                        id,
1185                        discovered_at,
1186                        identifier,
1187                        identifier_type.unwrap_or_else(|| "unknown".to_string()),
1188                        metadata,
1189                        resolution_status.unwrap_or_else(|| "not-requested".to_string()),
1190                        source.unwrap_or_else(|| "unknown".to_string()),
1191                        source_format.unwrap_or_else(|| "text".to_string()),
1192                    ],
1193                ),
1194                | Err(why) => Err(why),
1195            },
1196            | Err(why) => Err(why),
1197        }
1198    }
1199    fn build_select_query(&self, base: &str) -> SelectQuery {
1200        Self::build_select_query(self, base)
1201    }
1202}
1203impl Row for LicenseRow {
1204    fn table(&self) -> Table {
1205        Table::Licenses
1206    }
1207    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1208        let Self {
1209            identifier,
1210            name,
1211            is_deprecated,
1212            is_free_software,
1213            is_open_source,
1214            ..
1215        } = self.clone();
1216        let required = required5(identifier, name, is_deprecated, is_free_software, is_open_source);
1217        match required {
1218            | Ok((identifier, name, is_deprecated, is_free_software, is_open_source)) => match self.next_row_id(conn) {
1219                | Ok(id) => {
1220                    let params = backend::params![
1221                        id,
1222                        identifier,
1223                        name,
1224                        i32::from(is_deprecated),
1225                        i32::from(is_free_software),
1226                        i32::from(is_open_source)
1227                    ];
1228                    execute_insert(&self, conn, params)
1229                }
1230                | Err(why) => Err(why),
1231            },
1232            | Err(why) => Err(why),
1233        }
1234    }
1235    fn build_select_query(&self, base: &str) -> SelectQuery {
1236        Self::build_select_query(self, base)
1237    }
1238}
1239impl Row for LinkCacheRow {
1240    fn table(&self) -> Table {
1241        Table::LinkCache
1242    }
1243    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1244        let Self {
1245            url,
1246            status_code,
1247            is_reachable,
1248            checked_at,
1249            expires_at,
1250            ..
1251        } = self.clone();
1252        let checked_at = to_rfc3339(checked_at.unwrap_or_else(Timestamp::now));
1253        let required = required4(url, status_code, is_reachable, expires_at);
1254        match required {
1255            | Ok((url, status_code, is_reachable, expires_at)) => match self.next_row_id(conn) {
1256                | Ok(id) => {
1257                    let params = backend::params![id, url, status_code, i32::from(is_reachable), checked_at, to_rfc3339(expires_at)];
1258                    execute_insert(&self, conn, params)
1259                }
1260                | Err(why) => Err(why),
1261            },
1262            | Err(why) => Err(why),
1263        }
1264    }
1265    fn build_select_query(&self, base: &str) -> SelectQuery {
1266        Self::build_select_query(self, base)
1267    }
1268}
1269impl Row for ProgrammingLanguageRow {
1270    fn table(&self) -> Table {
1271        Table::ProgrammingLanguages
1272    }
1273    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1274        let Self {
1275            language_id,
1276            name,
1277            language_type,
1278            color,
1279            group_name,
1280            ..
1281        } = self.clone();
1282        let required = required1(name);
1283        match required {
1284            | Ok((name,)) => match self.next_row_id(conn) {
1285                | Ok(id) => {
1286                    let params = backend::params![id, language_id, name, language_type, color, group_name];
1287                    execute_insert(&self, conn, params)
1288                }
1289                | Err(why) => Err(why),
1290            },
1291            | Err(why) => Err(why),
1292        }
1293    }
1294    fn build_select_query(&self, base: &str) -> SelectQuery {
1295        Self::build_select_query(self, base)
1296    }
1297}
1298impl Row for ResearchActivityRow {
1299    fn table(&self) -> Table {
1300        Table::ResearchActivities
1301    }
1302    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1303        let Self {
1304            iid,
1305            rad_json,
1306            identity_keys_json,
1307            provenance_json,
1308            created_at,
1309            updated_at,
1310            ..
1311        } = self.clone();
1312        let created_at = to_rfc3339(created_at.unwrap_or_else(Timestamp::now));
1313        let updated_at = to_rfc3339(updated_at.unwrap_or_else(Timestamp::now));
1314        let required = required4(iid, rad_json, identity_keys_json, provenance_json);
1315        match required {
1316            | Ok((iid, rad_json, identity_keys_json, provenance_json)) => match self.next_row_id(conn) {
1317                | Ok(id) => {
1318                    let params = backend::params![id, iid, rad_json, identity_keys_json, provenance_json, created_at, updated_at];
1319                    execute_insert(&self, conn, params)
1320                }
1321                | Err(why) => Err(why),
1322            },
1323            | Err(why) => Err(why),
1324        }
1325    }
1326    fn build_select_query(&self, base: &str) -> SelectQuery {
1327        Self::build_select_query(self, base)
1328    }
1329}
1330impl Row for ValidationRow {
1331    fn table(&self) -> Table {
1332        Table::ValidationHistory
1333    }
1334    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1335        let Self {
1336            path,
1337            check_type,
1338            success,
1339            message,
1340            checked_at,
1341            ..
1342        } = self.clone();
1343        let success = i32::from(success.unwrap_or(true));
1344        let checked_at = to_rfc3339(checked_at.unwrap_or_else(Timestamp::now));
1345        let required = required3(path, check_type, message);
1346        match required {
1347            | Ok((path, check_type, message)) => match self.next_row_id(conn) {
1348                | Ok(id) => {
1349                    let params = backend::params![id, path, check_type, success, message, checked_at];
1350                    execute_insert(&self, conn, params)
1351                }
1352                | Err(why) => Err(why),
1353            },
1354            | Err(why) => Err(why),
1355        }
1356    }
1357    fn build_select_query(&self, base: &str) -> SelectQuery {
1358        Self::build_select_query(self, base)
1359    }
1360}
1361impl Row for ModelRow {
1362    fn table(&self) -> Table {
1363        Table::Models
1364    }
1365    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1366        let Self {
1367            model_id,
1368            name,
1369            family,
1370            variant,
1371            version,
1372            attachment,
1373            open_weights,
1374            reasoning,
1375            structured_output,
1376            temperature,
1377            tool_call,
1378            parameters,
1379            release_date,
1380            knowledge,
1381            last_updated,
1382            limit_context,
1383            limit_output,
1384            limit_input,
1385            modality_input,
1386            modality_output,
1387            cost_input,
1388            cost_output,
1389            cost_cache_read,
1390            cost_cache_write,
1391            cost_reasoning,
1392            cost_input_audio,
1393            cost_output_audio,
1394            cost_over_200k,
1395            cost_tiers,
1396            benchmarks,
1397            weights,
1398            ..
1399        } = self.clone();
1400        let required = required1(model_id);
1401        match required {
1402            | Ok((model_id,)) => match self.next_row_id(conn) {
1403                | Ok(id) => {
1404                    let params = backend::params![
1405                        id,
1406                        model_id,
1407                        name,
1408                        family,
1409                        variant,
1410                        version,
1411                        attachment.map(i32::from),
1412                        open_weights.map(i32::from),
1413                        reasoning.map(i32::from),
1414                        structured_output.map(i32::from),
1415                        temperature.map(i32::from),
1416                        tool_call.map(i32::from),
1417                        parameters,
1418                        release_date,
1419                        knowledge,
1420                        last_updated,
1421                        limit_context,
1422                        limit_output,
1423                        limit_input,
1424                        modality_input,
1425                        modality_output,
1426                        cost_input,
1427                        cost_output,
1428                        cost_cache_read,
1429                        cost_cache_write,
1430                        cost_reasoning,
1431                        cost_input_audio,
1432                        cost_output_audio,
1433                        cost_over_200k,
1434                        cost_tiers,
1435                        benchmarks,
1436                        weights,
1437                    ];
1438                    execute_insert(&self, conn, params)
1439                }
1440                | Err(why) => Err(why),
1441            },
1442            | Err(why) => Err(why),
1443        }
1444    }
1445    fn build_select_query(&self, base: &str) -> SelectQuery {
1446        Self::build_select_query(self, base)
1447    }
1448}
1449impl Row for ProviderRow {
1450    fn table(&self) -> Table {
1451        Table::Providers
1452    }
1453    fn insert(self, conn: &Connection) -> ApiResult<usize> {
1454        let Self {
1455            provider_id,
1456            name,
1457            description,
1458            endpoint,
1459            documentation,
1460            authentication,
1461            env,
1462            npm,
1463            url,
1464            established_date,
1465            last_updated,
1466            models,
1467            ..
1468        } = self.clone();
1469        let required = required1(provider_id);
1470        match required {
1471            | Ok((provider_id,)) => match self.next_row_id(conn) {
1472                | Ok(id) => {
1473                    let params = backend::params![
1474                        id,
1475                        provider_id,
1476                        name,
1477                        description,
1478                        endpoint,
1479                        documentation,
1480                        authentication,
1481                        env,
1482                        npm,
1483                        url,
1484                        established_date,
1485                        last_updated,
1486                        models,
1487                    ];
1488                    execute_insert(&self, conn, params)
1489                }
1490                | Err(why) => Err(why),
1491            },
1492            | Err(why) => Err(why),
1493        }
1494    }
1495    fn build_select_query(&self, base: &str) -> SelectQuery {
1496        Self::build_select_query(self, base)
1497    }
1498}
1499impl LinkCacheRow {
1500    /// Check if the cache entry is expired
1501    pub fn is_expired(&self) -> bool {
1502        self.expires_at.map(|expires_at| Timestamp::now() > expires_at).unwrap_or(true)
1503    }
1504}
1505fn display_or_none<T>(value: Option<&T>) -> String
1506where
1507    T: fmt::Display,
1508{
1509    value.map(ToString::to_string).unwrap_or_else(|| "None".to_string())
1510}
1511fn execute_insert<R, P>(row: &R, conn: &Connection, params: P) -> ApiResult<usize>
1512where
1513    R: Row,
1514    P: Params,
1515{
1516    let table_name = row.table().name();
1517    let table_columns = <R as Row>::fields();
1518    let fields = table_columns.join(", ");
1519    let placeholders = table_columns.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
1520    let query = format!("INSERT INTO {table_name} ({fields}) VALUES ({placeholders})");
1521    conn.execute(&query, params)
1522        .map_err(|why| eyre!("=> {} Failed to insert row: {why}", Label::fail()))
1523}
1524fn write_optional_field<T>(f: &mut fmt::Formatter<'_>, label: &str, value: Option<&T>) -> fmt::Result
1525where
1526    T: fmt::Display,
1527{
1528    match value {
1529        | Some(value) => write!(f, ", {label}: {value}"),
1530        | None => Ok(()),
1531    }
1532}
1533fn write_optional_timestamp(f: &mut fmt::Formatter<'_>, label: &str, value: Option<&Timestamp>) -> fmt::Result {
1534    match value {
1535        | Some(value) => write!(f, ", {label}: {}", value.strftime("%Y-%m-%d %H:%M:%S")),
1536        | None => Ok(()),
1537    }
1538}
1539#[cfg(feature = "duckdb")]
1540fn id_column_definition() -> &'static str {
1541    "id BIGINT PRIMARY KEY"
1542}
1543
1544#[cfg(not(feature = "duckdb"))]
1545fn id_column_definition() -> &'static str {
1546    "id INTEGER PRIMARY KEY AUTOINCREMENT"
1547}