Skip to main content

csp_format/
schema.rs

1//! CSP's own schema registry inside every .clip / .cmc database.
2//!
3//! - `ParamScheme`: one row per column CSP knows (`TableName`, `LabelName`),
4//!   with a data type code, an owner element kind and, for id columns, the
5//!   table the id points into (`LinkTable`). It lists every table of the
6//!   saving CSP version, also tables the file does not contain.
7//! - `ElemScheme`: one row per table with its element kind (`ElemType`) and
8//!   the highest `MainId` handed out so far (`MaxIndex`).
9//! - `ExternalTableAndColumnName`: the columns whose values are external
10//!   chunk ids (pixel blocks, vector data, 3D data, time-lapse data).
11//!
12//! See `docs/spec/02-database.md` for what the codes mean.
13
14use std::collections::{BTreeMap, HashMap};
15
16use rusqlite::Connection;
17
18use crate::database::{self, select_all};
19use crate::error::Result;
20
21/// `ParamScheme.DataType`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
23#[non_exhaustive]
24pub enum DataType {
25    /// 1.
26    Integer,
27    /// 2.
28    Real,
29    /// 3.
30    Text,
31    /// 4.
32    Blob,
33    /// Any other code.
34    Unknown(i64),
35}
36
37impl DataType {
38    /// The type of a stored `DataType` code.
39    pub fn from_code(code: i64) -> Self {
40        match code {
41            1 => DataType::Integer,
42            2 => DataType::Real,
43            3 => DataType::Text,
44            4 => DataType::Blob,
45            other => DataType::Unknown(other),
46        }
47    }
48
49    /// The stored code.
50    pub fn code(&self) -> i64 {
51        match self {
52            DataType::Integer => 1,
53            DataType::Real => 2,
54            DataType::Text => 3,
55            DataType::Blob => 4,
56            DataType::Unknown(code) => *code,
57        }
58    }
59
60    /// The SQLite declared type CSP uses for new columns of this type.
61    pub fn sql_type(&self) -> &'static str {
62        match self {
63            DataType::Integer => "INTEGER",
64            DataType::Real => "REAL",
65            DataType::Text => "TEXT",
66            DataType::Blob => "BLOB",
67            DataType::Unknown(_) => "",
68        }
69    }
70}
71
72/// Element kinds (`ElemScheme.ElemType`, `ParamScheme.OwnerType`). An
73/// owner column holds the `MainId` of the element of that kind that owns
74/// the row (for example `Offscreen.LayerId`, owner kind 4 = layer).
75pub fn element_kind_name(code: i64) -> &'static str {
76    match code {
77        0 => "none",
78        1 => "project",
79        2 => "canvas",
80        4 => "layer",
81        8 => "layer_object",
82        1024 => "bank",
83        2048 => "item",
84        4096 => "track",
85        _ => "unknown",
86    }
87}
88
89/// One `ParamScheme` row.
90#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
91pub struct Param {
92    /// `TableName`.
93    pub table: String,
94    /// `LabelName`: the column.
95    pub column: String,
96    /// `DataType`.
97    pub data_type: DataType,
98    /// `Flag`, as stored.
99    pub flag: i64,
100    /// `OwnerType`: the element kind whose `MainId` the column holds (see [`element_kind_name`]).
101    pub owner_type: i64,
102    /// `LockType`, as stored (see `docs/spec/09-versions.md`).
103    pub lock_type: i64,
104    /// `LockSpecified`, as stored.
105    pub lock_specified: i64,
106    /// `AlternativeLockType`, in newer files.
107    pub alternative_lock_type: Option<i64>,
108    /// `AlternativeLockSpecified`, in newer files.
109    pub alternative_lock_specified: Option<i64>,
110    /// Table an id column links to (`MainId` there); None for plain values.
111    pub link_table: Option<String>,
112}
113
114/// One `ElemScheme` row.
115#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
116pub struct Elem {
117    /// `TableName`.
118    pub table: String,
119    /// `ElemType` (see [`element_kind_name`]).
120    pub elem_type: i64,
121    /// `MaxIndex`: the highest `MainId` given out in the table.
122    pub max_index: i64,
123}
124
125/// The registry of one database.
126#[derive(Debug, Clone, Default, serde::Serialize)]
127pub struct Schema {
128    /// `ParamScheme` rows.
129    pub params: Vec<Param>,
130    /// `ElemScheme` rows.
131    pub elems: Vec<Elem>,
132    /// `ExternalTableAndColumnName`: (table, column) pairs that hold external chunk ids.
133    pub external_columns: Vec<(String, String)>,
134}
135
136impl Schema {
137    /// Reads the three registry tables.
138    pub fn read(connection: &Connection) -> Result<Self> {
139        let params = select_all(connection, "ParamScheme")?
140            .iter()
141            .map(|row| Param {
142                table: row.text("TableName").unwrap_or_default(),
143                column: row.text("LabelName").unwrap_or_default(),
144                data_type: DataType::from_code(row.int("DataType")),
145                flag: row.int("Flag"),
146                owner_type: row.int("OwnerType"),
147                lock_type: row.int("LockType"),
148                lock_specified: row.int("LockSpecified"),
149                alternative_lock_type: row.opt_int("AlternativeLockType"),
150                alternative_lock_specified: row.opt_int("AlternativeLockSpecified"),
151                link_table: row.text("LinkTable").filter(|text| !text.is_empty()),
152            })
153            .collect();
154        let elems = select_all(connection, "ElemScheme")?
155            .iter()
156            .map(|row| Elem {
157                table: row.text("TableName").unwrap_or_default(),
158                elem_type: row.int("ElemType"),
159                max_index: row.int("MaxIndex"),
160            })
161            .collect();
162        let external_columns = select_all(connection, "ExternalTableAndColumnName")?
163            .iter()
164            .map(|row| (row.text("TableName").unwrap_or_default(), row.text("ColumnName").unwrap_or_default()))
165            .collect();
166        Ok(Schema { params, elems, external_columns })
167    }
168
169    /// The row of a column.
170    pub fn param(&self, table: &str, column: &str) -> Option<&Param> {
171        self.params.iter().find(|param| param.table == table && param.column == column)
172    }
173
174    /// The rows of a table.
175    pub fn table_params(&self, table: &str) -> impl Iterator<Item = &Param> {
176        self.params.iter().filter(move |param| param.table == table)
177    }
178
179    /// The row of a table.
180    pub fn elem(&self, table: &str) -> Option<&Elem> {
181        self.elems.iter().find(|elem| elem.table == table)
182    }
183
184    /// Whether the column holds external chunk ids.
185    pub fn is_external(&self, table: &str, column: &str) -> bool {
186        self.external_columns.iter().any(|(t, c)| t == table && c == column)
187    }
188
189    /// Id links: (table, column) → linked table.
190    pub fn links(&self) -> BTreeMap<(String, String), String> {
191        self.params
192            .iter()
193            .filter_map(|param| Some(((param.table.clone(), param.column.clone()), param.link_table.clone()?)))
194            .collect()
195    }
196
197    /// Tables in registry order.
198    pub fn table_names(&self) -> Vec<String> {
199        let mut seen = HashMap::new();
200        let mut names = Vec::new();
201        for param in &self.params {
202            if seen.insert(param.table.clone(), ()).is_none() {
203                names.push(param.table.clone());
204            }
205        }
206        names
207    }
208}
209
210/// Hand out the next `MainId` of `table`, as CSP does: one more than both
211/// `ElemScheme.MaxIndex` and the highest id in the table, recorded back
212/// into `ElemScheme`.
213pub fn allocate_main_id(connection: &Connection, table: &str) -> Result<i64> {
214    let recorded: i64 = connection
215        .query_row("SELECT MaxIndex FROM ElemScheme WHERE TableName = ?1", [table], |row| row.get::<_, Option<i64>>(0))
216        .ok()
217        .flatten()
218        .unwrap_or(0);
219    let used: i64 = if database::table_exists(connection, table)? {
220        connection.query_row(
221            &format!("SELECT COALESCE(MAX(MainId), 0) FROM {}", database::quote(table)),
222            [],
223            |row| row.get(0),
224        )?
225    } else {
226        0
227    };
228    let next = recorded.max(used) + 1;
229    let updated = connection.execute("UPDATE ElemScheme SET MaxIndex = ?1 WHERE TableName = ?2", (next, table))?;
230    if updated == 0 && database::table_exists(connection, "ElemScheme")? {
231        connection
232            .execute("INSERT INTO ElemScheme(TableName, ElemType, MaxIndex) VALUES (?1, 0, ?2)", (table, next))?;
233    }
234    Ok(next)
235}