1use rusqlite::{params, Connection, Transaction};
9use std::os::raw::c_char;
10
11use crate::db;
12use crate::types::*;
13
14#[derive(Debug)]
15pub enum SerializeError {
16 Db(db::DbError),
17 Sqlite(rusqlite::Error),
18 Io(String),
19}
20
21impl std::fmt::Display for SerializeError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 SerializeError::Db(e) => write!(f, "db: {e}"),
25 SerializeError::Sqlite(e) => write!(f, "sqlite: {e}"),
26 SerializeError::Io(e) => write!(f, "io: {e}"),
27 }
28 }
29}
30
31impl std::error::Error for SerializeError {}
32
33impl From<db::DbError> for SerializeError {
34 fn from(e: db::DbError) -> Self { SerializeError::Db(e) }
35}
36impl From<rusqlite::Error> for SerializeError {
37 fn from(e: rusqlite::Error) -> Self { SerializeError::Sqlite(e) }
38}
39impl From<std::io::Error> for SerializeError {
40 fn from(e: std::io::Error) -> Self { SerializeError::Io(e.to_string()) }
41}
42
43pub type SerializeResult<T> = Result<T, SerializeError>;
44
45const LARGE_EMBEDDED_FILES_THRESHOLD_BYTES: usize = 128 * 1024 * 1024;
46
47pub fn serialize(state: &ExportedDataState) -> SerializeResult<Vec<u8>> {
54 let conn = db::open_memory_compact()?;
55 let mut inner = conn.into_inner();
56 let embedded_external_bytes = total_external_file_data_bytes(state);
57 let should_optimize_for_large_embeds = embedded_external_bytes >= LARGE_EMBEDDED_FILES_THRESHOLD_BYTES;
58
59 {
60 inner.execute_batch("PRAGMA foreign_keys = OFF;")?;
66
67 let tx = inner.transaction()?;
68 write_document(&tx, state)?;
69 write_charter(&tx, &state.charter)?;
70 write_issues(&tx, &state.issues)?;
71 write_global_state(&tx, &state.duc_global_state)?;
72 write_local_state(&tx, &state.duc_local_state)?;
73 write_dictionary(&tx, &state.dictionary)?;
74 write_stack_and_containers(&tx, state)?;
75 write_blocks(&tx, state)?;
76 write_elements(&tx, state)?;
77 write_external_files(&tx, &state.external_files, &state.external_files_data)?;
78 write_version_graph(&tx, &state.version_graph)?;
79 tx.commit()?;
80
81 inner.execute_batch("PRAGMA foreign_keys = ON;")?;
82 }
83
84 if !should_optimize_for_large_embeds {
88 inner.execute_batch("VACUUM;")?;
89 }
90
91 let raw = export_db_bytes(&inner)?;
92
93 if should_optimize_for_large_embeds {
97 return Ok(raw);
98 }
99
100 compress_duc_bytes(&raw)
101}
102
103fn total_external_file_data_bytes(state: &ExportedDataState) -> usize {
104 state
105 .external_files_data
106 .as_ref()
107 .map(|data| data.values().map(|blob| blob.len()).sum())
108 .unwrap_or(0)
109}
110
111fn export_db_bytes(conn: &Connection) -> SerializeResult<Vec<u8>> {
114 let schema = b"main\0";
117
118 let mut size: rusqlite::ffi::sqlite3_int64 = 0;
119 let ptr = unsafe {
120 rusqlite::ffi::sqlite3_serialize(
121 conn.handle(),
122 schema.as_ptr() as *const c_char,
123 &mut size,
124 0,
125 )
126 };
127
128 if ptr.is_null() || size < 0 {
129 return Err(SerializeError::Io(
130 "sqlite3_serialize failed to export database bytes".into(),
131 ));
132 }
133
134 let bytes = unsafe {
135 let slice = std::slice::from_raw_parts(ptr as *const u8, size as usize);
136 let out = slice.to_vec();
137 rusqlite::ffi::sqlite3_free(ptr as *mut std::ffi::c_void);
138 out
139 };
140
141 Ok(bytes)
142}
143
144fn compress_duc_bytes(raw: &[u8]) -> SerializeResult<Vec<u8>> {
146 use flate2::write::GzEncoder;
147 use flate2::Compression;
148 use std::io::Write;
149
150 let mut encoder = GzEncoder::new(Vec::with_capacity(raw.len() / 4), Compression::default());
151 encoder.write_all(raw)?;
152 Ok(encoder.finish()?)
153}
154
155fn write_document(tx: &Transaction, state: &ExportedDataState) -> SerializeResult<()> {
158 tx.execute(
159 "INSERT OR REPLACE INTO duc_document (id, version, source, data_type, thumbnail)
160 VALUES (?1, ?2, ?3, ?4, ?5)",
161 params![
162 state.id.as_deref().unwrap_or(""),
163 state.version,
164 state.source,
165 state.data_type,
166 state.thumbnail.as_deref(),
167 ],
168 )?;
169 Ok(())
170}
171
172fn charter_phase_to_str(phase: DucCharterPhase) -> &'static str {
173 match phase {
174 DucCharterPhase::Intent => "intent",
175 DucCharterPhase::Review => "review",
176 DucCharterPhase::Delivery => "delivery",
177 DucCharterPhase::Closed => "closed",
178 }
179}
180
181fn issue_status_to_str(status: DucIssueStatus) -> &'static str {
182 match status {
183 DucIssueStatus::Open => "open",
184 DucIssueStatus::Closed => "closed",
185 DucIssueStatus::Dismissed => "dismissed",
186 }
187}
188
189fn write_charter(tx: &Transaction, charter: &Option<DucCharter>) -> SerializeResult<()> {
190 let Some(charter) = charter else { return Ok(()) };
191
192 tx.execute(
193 "INSERT OR REPLACE INTO duc_charter
194 (id, title, description, objective, phase, closed_reason, updated_at)
195 VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6)",
196 params![
197 charter.title,
198 charter.description,
199 charter.objective,
200 charter_phase_to_str(charter.phase),
201 charter.closed_reason,
202 charter.updated_at,
203 ],
204 )?;
205
206 let mut req_stmt = tx.prepare_cached(
207 "INSERT INTO duc_charter_requirements (id, statement, must, sort_order)
208 VALUES (?1, ?2, ?3, ?4)"
209 )?;
210 let mut criteria_stmt = tx.prepare_cached(
211 "INSERT INTO duc_charter_requirement_acceptance_criteria (requirement_id, sort_order, criterion)
212 VALUES (?1, ?2, ?3)"
213 )?;
214 for (i, requirement) in charter.requirements.iter().enumerate() {
215 req_stmt.execute(params![requirement.id, requirement.statement, requirement.must as i32, i as i32])?;
216 if let Some(criteria) = &requirement.acceptance_criteria {
217 for (j, criterion) in criteria.iter().enumerate() {
218 criteria_stmt.execute(params![requirement.id, j as i32, criterion])?;
219 }
220 }
221 }
222
223 let mut constraint_stmt = tx.prepare_cached(
224 "INSERT INTO duc_charter_constraints (id, statement, hard, sort_order)
225 VALUES (?1, ?2, ?3, ?4)"
226 )?;
227 for (i, constraint) in charter.constraints.iter().enumerate() {
228 constraint_stmt.execute(params![constraint.id, constraint.statement, constraint.hard as i32, i as i32])?;
229 }
230
231 let mut decision_stmt = tx.prepare_cached(
232 "INSERT INTO duc_charter_decisions (id, accepted, decision, rationale, decided_at, sort_order)
233 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
234 )?;
235 let mut decision_issue_stmt = tx.prepare_cached(
236 "INSERT INTO duc_charter_decision_issue_ids (decision_id, issue_id, sort_order)
237 VALUES (?1, ?2, ?3)"
238 )?;
239 for (i, decision) in charter.decisions.iter().enumerate() {
240 decision_stmt.execute(params![
241 decision.id,
242 decision.accepted as i32,
243 decision.decision,
244 decision.rationale,
245 decision.decided_at,
246 i as i32,
247 ])?;
248 if let Some(issue_ids) = &decision.issue_ids {
249 for (j, issue_id) in issue_ids.iter().enumerate() {
250 decision_issue_stmt.execute(params![decision.id, issue_id, j as i32])?;
251 }
252 }
253 }
254
255 if let Some(stakeholders) = &charter.stakeholders {
256 let mut stakeholder_stmt = tx.prepare_cached(
257 "INSERT INTO duc_charter_stakeholders (sort_order, actor_identifier, actor_name, role)
258 VALUES (?1, ?2, ?3, ?4)"
259 )?;
260 for (i, stakeholder) in stakeholders.iter().enumerate() {
261 stakeholder_stmt.execute(params![
262 i as i32,
263 stakeholder.actor.identifier,
264 stakeholder.actor.name,
265 stakeholder.role,
266 ])?;
267 }
268 }
269
270 Ok(())
271}
272
273fn write_issues(tx: &Transaction, issues: &[DucIssue]) -> SerializeResult<()> {
274 let mut issue_stmt = tx.prepare_cached(
275 "INSERT INTO duc_issues
276 (id, local_id, title, status, dismissed_reason, due_date, author_id,
277 created_at, updated_at, deleted_at, sort_order)
278 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"
279 )?;
280
281 for (i, issue) in issues.iter().enumerate() {
282 issue_stmt.execute(params![
283 issue.id,
284 issue.local_id,
285 issue.title,
286 issue_status_to_str(issue.status),
287 issue.dismissed_reason,
288 issue.due_date,
289 issue.author_id,
290 issue.created_at,
291 issue.updated_at,
292 issue.deleted_at,
293 i as i32,
294 ])?;
295 write_issue_actor_ids(tx, "duc_issue_assignees", &issue.id, issue.assignee_ids.as_deref())?;
296 write_issue_actor_ids(tx, "duc_issue_followers", &issue.id, issue.follower_ids.as_deref())?;
297 write_issue_messages(tx, &issue.id, &issue.messages)?;
298 if let Some(anchor) = &issue.anchor {
299 write_issue_anchor(tx, &issue.id, anchor)?;
300 }
301 }
302
303 Ok(())
304}
305
306fn write_issue_actor_ids(
307 tx: &Transaction,
308 table: &str,
309 issue_id: &str,
310 actor_ids: Option<&[String]>,
311) -> SerializeResult<()> {
312 let Some(actor_ids) = actor_ids else { return Ok(()) };
313 let sql = format!(
314 "INSERT OR REPLACE INTO {table} (issue_id, actor_identifier, sort_order) VALUES (?1, ?2, ?3)"
315 );
316 let mut stmt = tx.prepare_cached(&sql)?;
317 for (i, actor_id) in actor_ids.iter().enumerate() {
318 stmt.execute(params![issue_id, actor_id, i as i32])?;
319 }
320 Ok(())
321}
322
323fn write_issue_messages(tx: &Transaction, issue_id: &str, messages: &[DucIssueMessage]) -> SerializeResult<()> {
324 let mut message_stmt = tx.prepare_cached(
325 "INSERT INTO duc_issue_messages
326 (id, issue_id, author_identifier, author_name, content, reply_to_id,
327 created_at, edited_at, deleted_at, sort_order)
328 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"
329 )?;
330 let mut reaction_stmt = tx.prepare_cached(
331 "INSERT OR REPLACE INTO duc_issue_message_reactions
332 (message_id, emoji, actor_identifier, sort_order)
333 VALUES (?1, ?2, ?3, ?4)"
334 )?;
335
336 for (i, message) in messages.iter().enumerate() {
337 message_stmt.execute(params![
338 message.id,
339 issue_id,
340 message.author.identifier,
341 message.author.name,
342 message.content,
343 message.reply_to_id,
344 message.created_at,
345 message.edited_at,
346 message.deleted_at,
347 i as i32,
348 ])?;
349 if let Some(reactions) = &message.reactions {
350 for (emoji, actor_ids) in reactions {
351 for (j, actor_id) in actor_ids.iter().enumerate() {
352 reaction_stmt.execute(params![message.id, emoji, actor_id, j as i32])?;
353 }
354 }
355 }
356 }
357
358 Ok(())
359}
360
361fn write_issue_anchor(tx: &Transaction, issue_id: &str, anchor: &DucIssueAnchor) -> SerializeResult<()> {
362 match anchor {
363 DucIssueAnchor::Canvas { x, y, scope } => {
364 tx.execute(
365 "INSERT INTO duc_issue_anchors (issue_id, anchor_type, canvas_x, canvas_y, canvas_scope)
366 VALUES (?1, 'canvas', ?2, ?3, ?4)",
367 params![issue_id, x, y, scope],
368 )?;
369 }
370 DucIssueAnchor::Element { element_id, anchor_x, anchor_y } => {
371 tx.execute(
372 "INSERT INTO duc_issue_anchors (issue_id, anchor_type, element_id, anchor_x, anchor_y)
373 VALUES (?1, 'element', ?2, ?3, ?4)",
374 params![issue_id, element_id, anchor_x, anchor_y],
375 )?;
376 }
377 DucIssueAnchor::Model { element_id, point, normal, viewer_state, topology_id } => {
378 tx.execute(
379 "INSERT INTO duc_issue_anchors (
380 issue_id, anchor_type, element_id,
381 model_point_x, model_point_y, model_point_z,
382 model_normal_x, model_normal_y, model_normal_z, topology_id
383 ) VALUES (?1, 'model', ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
384 params![
385 issue_id,
386 element_id,
387 point[0], point[1], point[2],
388 normal.as_ref().map(|n| n[0]),
389 normal.as_ref().map(|n| n[1]),
390 normal.as_ref().map(|n| n[2]),
391 topology_id,
392 ],
393 )?;
394 if let Some(viewer_state) = viewer_state {
395 write_model_viewer_state(tx, "issue_anchor", issue_id, viewer_state)?;
396 }
397 }
398 }
399 Ok(())
400}
401
402fn write_global_state(tx: &Transaction, gs: &Option<DucGlobalState>) -> SerializeResult<()> {
405 let Some(gs) = gs else { return Ok(()) };
406 tx.execute(
407 "INSERT OR REPLACE INTO duc_global_state
408 (id, view_background_color, main_scope, scope_exponent_threshold)
409 VALUES (1, ?1, ?2, ?3)",
410 params![
411 gs.view_background_color,
412 gs.main_scope,
413 gs.scope_exponent_threshold,
414 ],
415 )?;
416 Ok(())
417}
418
419fn write_local_state(tx: &Transaction, ls: &Option<DucLocalState>) -> SerializeResult<()> {
422 let Some(ls) = ls else { return Ok(()) };
423 tx.execute(
424 "INSERT OR REPLACE INTO duc_local_state (
425 id, scope, scroll_x, scroll_y, zoom,
426 is_binding_enabled,
427 current_item_opacity, current_item_font_family, current_item_font_size,
428 current_item_text_align, current_item_roundness,
429 start_head_type, start_head_block_id, start_head_size,
430 end_head_type, end_head_block_id, end_head_size,
431 pen_mode, view_mode_enabled, objects_snap_mode_enabled,
432 grid_mode_enabled, outline_mode_enabled, manual_save_mode,
433 decimal_places
434 ) VALUES (
435 1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10,
436 ?11, ?12, ?13, ?14, ?15, ?16,
437 ?17, ?18, ?19, ?20, ?21, ?22, ?23
438 )",
439 params![
440 ls.scope,
441 ls.scroll_x,
442 ls.scroll_y,
443 ls.zoom,
444 ls.is_binding_enabled as i32,
445 ls.current_item_opacity,
446 ls.current_item_font_family,
447 ls.current_item_font_size,
448 ls.current_item_text_align as i32,
449 ls.current_item_roundness,
450 ls.current_item_start_line_head.as_ref().and_then(|h| h.head_type.map(|t| t as i32)),
451 ls.current_item_start_line_head.as_ref().and_then(|h| h.block_id.clone()),
452 ls.current_item_start_line_head.as_ref().map(|h| h.size),
453 ls.current_item_end_line_head.as_ref().and_then(|h| h.head_type.map(|t| t as i32)),
454 ls.current_item_end_line_head.as_ref().and_then(|h| h.block_id.clone()),
455 ls.current_item_end_line_head.as_ref().map(|h| h.size),
456 ls.pen_mode as i32,
457 ls.view_mode_enabled as i32,
458 ls.objects_snap_mode_enabled as i32,
459 ls.grid_mode_enabled as i32,
460 ls.outline_mode_enabled as i32,
461 ls.manual_save_mode as i32,
462 ls.decimal_places,
463 ],
464 )?;
465
466 if let Some(ref stroke) = ls.current_item_stroke {
467 write_stroke(tx, "local_state", "1", 0, stroke)?;
468 }
469 if let Some(ref bg) = ls.current_item_background {
470 write_background(tx, "local_state", "1", 0, bg)?;
471 }
472
473 Ok(())
474}
475
476fn write_dictionary(tx: &Transaction, dict: &Option<std::collections::HashMap<String, String>>) -> SerializeResult<()> {
479 let Some(dict) = dict else { return Ok(()) };
480 let mut stmt = tx.prepare_cached(
481 "INSERT OR REPLACE INTO document_dictionary (key, value) VALUES (?1, ?2)"
482 )?;
483 for (key, value) in dict {
484 stmt.execute(params![key, value])?;
485 }
486 Ok(())
487}
488
489fn write_stack_base(tx: &Transaction, id: &str, sb: &DucStackBase) -> SerializeResult<()> {
492 tx.execute(
493 "INSERT OR REPLACE INTO stack_properties
494 (id, label, description, is_collapsed, is_plot, is_visible, locked, opacity)
495 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
496 params![
497 id,
498 sb.label,
499 sb.description,
500 sb.is_collapsed as i32,
501 sb.is_plot as i32,
502 sb.is_visible as i32,
503 sb.locked as i32,
504 sb.styles.opacity,
505 ],
506 )?;
507 Ok(())
508}
509
510fn write_stack_and_containers(tx: &Transaction, state: &ExportedDataState) -> SerializeResult<()> {
511 for layer in &state.layers {
512 write_stack_base(tx, &layer.id, &layer.stack_base)?;
513 tx.execute(
514 "INSERT OR REPLACE INTO layers (id, readonly) VALUES (?1, ?2)",
515 params![layer.id, layer.readonly as i32],
516 )?;
517 if let Some(ref ov) = layer.overrides {
518 write_stroke(tx, "layer", &layer.id, 0, &ov.stroke)?;
519 write_background(tx, "layer", &layer.id, 0, &ov.background)?;
520 }
521 }
522 for group in &state.groups {
523 write_stack_base(tx, &group.id, &group.stack_base)?;
524 tx.execute(
525 "INSERT OR REPLACE INTO groups (id) VALUES (?1)",
526 params![group.id],
527 )?;
528 }
529 for region in &state.regions {
530 write_stack_base(tx, ®ion.id, ®ion.stack_base)?;
531 tx.execute(
532 "INSERT OR REPLACE INTO regions (id, boolean_operation) VALUES (?1, ?2)",
533 params![region.id, region.boolean_operation as i32],
534 )?;
535 }
536 Ok(())
537}
538
539fn write_blocks(tx: &Transaction, state: &ExportedDataState) -> SerializeResult<()> {
542 for block in &state.blocks {
543 tx.execute(
544 "INSERT OR REPLACE INTO blocks (id, label, description, version)
545 VALUES (?1, ?2, ?3, ?4)",
546 params![block.id, block.label, block.description, block.version],
547 )?;
548 if let Some(ref meta) = block.metadata {
549 write_block_metadata(tx, "block", &block.id, meta, block.thumbnail.as_deref())?;
550 }
551 }
552
553 for inst in &state.block_instances {
554 let (dup_rows, dup_cols, dup_row_sp, dup_col_sp) = match &inst.duplication_array {
555 Some(da) => (Some(da.rows), Some(da.cols), Some(da.row_spacing), Some(da.col_spacing)),
556 None => (None, None, None, None),
557 };
558 tx.execute(
559 "INSERT OR REPLACE INTO block_instances
560 (id, block_id, version, dup_rows, dup_cols, dup_row_spacing, dup_col_spacing)
561 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
562 params![inst.id, inst.block_id, inst.version, dup_rows, dup_cols, dup_row_sp, dup_col_sp],
563 )?;
564 if let Some(ref overrides) = inst.element_overrides {
565 let mut stmt = tx.prepare_cached(
566 "INSERT OR REPLACE INTO block_instance_overrides (instance_id, key, value)
567 VALUES (?1, ?2, ?3)"
568 )?;
569 for ov in overrides {
570 stmt.execute(params![inst.id, ov.key, ov.value])?;
571 }
572 }
573 }
574
575 for col in &state.block_collections {
576 tx.execute(
577 "INSERT OR REPLACE INTO block_collections (id, label) VALUES (?1, ?2)",
578 params![col.id, col.label],
579 )?;
580 if let Some(ref meta) = col.metadata {
581 write_block_metadata(tx, "collection", &col.id, meta, col.thumbnail.as_deref())?;
582 }
583 let mut stmt = tx.prepare_cached(
584 "INSERT OR REPLACE INTO block_collection_entries (collection_id, child_id, is_collection)
585 VALUES (?1, ?2, ?3)"
586 )?;
587 for child in &col.children {
588 stmt.execute(params![col.id, child.id, child.is_collection as i32])?;
589 }
590 }
591
592 Ok(())
593}
594
595fn write_block_metadata(
596 tx: &Transaction,
597 owner_type: &str,
598 owner_id: &str,
599 meta: &DucBlockMetadata,
600 thumbnail: Option<&[u8]>,
601) -> SerializeResult<()> {
602 tx.execute(
603 "INSERT OR REPLACE INTO block_metadata
604 (owner_type, owner_id, source, usage_count, created_at, updated_at, localization, thumbnail)
605 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
606 params![
607 owner_type,
608 owner_id,
609 meta.source,
610 meta.usage_count,
611 meta.created_at,
612 meta.updated_at,
613 meta.localization,
614 thumbnail,
615 ],
616 )?;
617 Ok(())
618}
619
620fn write_elements(tx: &Transaction, state: &ExportedDataState) -> SerializeResult<()> {
623 for wrapper in &state.elements {
624 write_element_wrapper(tx, wrapper)?;
625 }
626 Ok(())
627}
628
629fn write_element_wrapper(tx: &Transaction, wrapper: &ElementWrapper) -> SerializeResult<()> {
630 match &wrapper.element {
631 DucElementEnum::DucRectangleElement(e) => {
632 write_base_element(tx, "rectangle", &e.base)?;
633 }
634 DucElementEnum::DucPolygonElement(e) => {
635 write_base_element(tx, "polygon", &e.base)?;
636 tx.execute(
637 "INSERT INTO element_polygon (element_id, sides) VALUES (?1, ?2)",
638 params![e.base.id, e.sides],
639 )?;
640 }
641 DucElementEnum::DucEllipseElement(e) => {
642 write_base_element(tx, "ellipse", &e.base)?;
643 tx.execute(
644 "INSERT INTO element_ellipse (element_id, ratio, start_angle, end_angle, show_aux_crosshair)
645 VALUES (?1, ?2, ?3, ?4, ?5)",
646 params![e.base.id, e.ratio, e.start_angle, e.end_angle, e.show_aux_crosshair as i32],
647 )?;
648 }
649 DucElementEnum::DucEmbeddableElement(e) => {
650 write_base_element(tx, "embeddable", &e.base)?;
651 tx.execute(
652 "INSERT INTO element_embeddable (element_id) VALUES (?1)",
653 params![e.base.id],
654 )?;
655 }
656 DucElementEnum::DucTextElement(e) => {
657 write_base_element(tx, "text", &e.base)?;
658 write_text_element(tx, e)?;
659 }
660 DucElementEnum::DucImageElement(e) => {
661 write_base_element(tx, "image", &e.base)?;
662 write_image_element(tx, e)?;
663 }
664 DucElementEnum::DucFreeDrawElement(e) => {
665 write_base_element(tx, "freedraw", &e.base)?;
666 write_freedraw_element(tx, e)?;
667 }
668 DucElementEnum::DucLinearElement(e) => {
669 write_base_element(tx, "line", &e.linear_base.base)?;
670 write_linear_element(tx, &e.linear_base, e.wipeout_below, false)?;
671 }
672 DucElementEnum::DucArrowElement(e) => {
673 write_base_element(tx, "arrow", &e.linear_base.base)?;
674 write_linear_element(tx, &e.linear_base, false, e.elbowed)?;
675 }
676 DucElementEnum::DucFrameElement(e) => {
677 write_base_element(tx, "frame", &e.stack_element_base.base)?;
678 write_stack_element_base(tx, &e.stack_element_base)?;
679 tx.execute(
680 "INSERT INTO element_frame (element_id) VALUES (?1)",
681 params![e.stack_element_base.base.id],
682 )?;
683 }
684 DucElementEnum::DucPlotElement(e) => {
685 write_base_element(tx, "plot", &e.stack_element_base.base)?;
686 write_stack_element_base(tx, &e.stack_element_base)?;
687 tx.execute(
688 "INSERT INTO element_plot (element_id, margin_top, margin_right, margin_bottom, margin_left)
689 VALUES (?1, ?2, ?3, ?4, ?5)",
690 params![
691 e.stack_element_base.base.id,
692 e.layout.margins.top,
693 e.layout.margins.right,
694 e.layout.margins.bottom,
695 e.layout.margins.left,
696 ],
697 )?;
698 }
699 DucElementEnum::DucPdfElement(e) => {
700 write_base_element(tx, "pdf", &e.base)?;
701 write_document_grid_config(tx, &e.base.id, e.file_id.as_deref(), &e.grid_config)?;
702 tx.execute(
703 "INSERT INTO element_pdf (element_id) VALUES (?1)",
704 params![e.base.id],
705 )?;
706 }
707 DucElementEnum::DucDocElement(e) => {
708 write_base_element(tx, "doc", &e.base)?;
709 write_document_grid_config(tx, &e.base.id, e.file_id.as_deref(), &e.grid_config)?;
710 tx.execute(
711 "INSERT INTO element_doc (element_id, text) VALUES (?1, ?2)",
712 params![e.base.id, e.text],
713 )?;
714 write_doc_referenced_file_ids(tx, &e.base.id, &e.referenced_file_ids)?;
715 }
716 DucElementEnum::DucTableElement(e) => {
717 write_base_element(tx, "table", &e.base)?;
718 write_document_grid_config(tx, &e.base.id, e.file_id.as_deref(), &e.grid_config)?;
719 tx.execute(
720 "INSERT INTO element_table (element_id) VALUES (?1)",
721 params![e.base.id],
722 )?;
723 }
724 DucElementEnum::DucModelElement(e) => {
725 write_base_element(tx, "model", &e.base)?;
726 write_model_element(tx, e)?;
727 }
728 }
729 Ok(())
730}
731
732fn write_base_element(tx: &Transaction, element_type: &str, base: &DucElementBase) -> SerializeResult<()> {
735 tx.execute(
736 "INSERT INTO elements (
737 id, element_type,
738 x, y, width, height, angle,
739 scope, label, description, is_visible,
740 seed, version, version_nonce, updated, \"index\",
741 is_plot, is_deleted,
742 roundness, blending, opacity,
743 instance_id, layer_id, frame_id,
744 z_index, link, locked, custom_data
745 ) VALUES (
746 ?1, ?2,
747 ?3, ?4, ?5, ?6, ?7,
748 ?8, ?9, ?10, ?11,
749 ?12, ?13, ?14, ?15, ?16,
750 ?17, ?18,
751 ?19, ?20, ?21,
752 ?22, ?23, ?24,
753 ?25, ?26, ?27, ?28
754 )",
755 params![
756 base.id, element_type,
757 base.x, base.y, base.width, base.height, base.angle,
758 base.scope, base.label, base.description, base.is_visible as i32,
759 base.seed, base.version, base.version_nonce, base.updated, base.index,
760 base.is_plot as i32, base.is_deleted as i32,
761 base.styles.roundness, base.styles.blending.map(|b| b as i32), base.styles.opacity,
762 base.instance_id, base.layer_id, base.frame_id,
763 base.z_index, base.link, base.locked as i32, base.custom_data,
764 ],
765 )?;
766
767 for (i, bg) in base.styles.background.iter().enumerate() {
768 write_background(tx, "element", &base.id, i as i32, bg)?;
769 }
770 for (i, st) in base.styles.stroke.iter().enumerate() {
771 write_stroke(tx, "element", &base.id, i as i32, st)?;
772 }
773
774 if let Some(ref bound) = base.bound_elements {
775 let mut stmt = tx.prepare_cached(
776 "INSERT INTO element_bound_elements (element_id, bound_element_id, bound_type, sort_order)
777 VALUES (?1, ?2, ?3, ?4)"
778 )?;
779 for (i, be) in bound.iter().enumerate() {
780 stmt.execute(params![base.id, be.id, be.element_type, i as i32])?;
781 }
782 }
783
784 {
785 let mut stmt = tx.prepare_cached(
786 "INSERT INTO element_group_memberships (element_id, group_id, sort_order) VALUES (?1, ?2, ?3)"
787 )?;
788 for (i, gid) in base.group_ids.iter().enumerate() {
789 stmt.execute(params![base.id, gid, i as i32])?;
790 }
791 }
792
793 {
794 let mut stmt = tx.prepare_cached(
795 "INSERT INTO element_block_memberships (element_id, block_id, sort_order) VALUES (?1, ?2, ?3)"
796 )?;
797 for (i, bid) in base.block_ids.iter().enumerate() {
798 stmt.execute(params![base.id, bid, i as i32])?;
799 }
800 }
801
802 {
803 let mut stmt = tx.prepare_cached(
804 "INSERT INTO element_region_memberships (element_id, region_id, sort_order) VALUES (?1, ?2, ?3)"
805 )?;
806 for (i, rid) in base.region_ids.iter().enumerate() {
807 stmt.execute(params![base.id, rid, i as i32])?;
808 }
809 }
810
811 Ok(())
812}
813
814fn write_stack_element_base(tx: &Transaction, seb: &DucStackElementBase) -> SerializeResult<()> {
817 tx.execute(
818 "INSERT INTO element_stack_properties
819 (element_id, label, description, is_collapsed, is_plot, is_visible, locked, opacity, clip, label_visible)
820 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
821 params![
822 seb.base.id,
823 seb.stack_base.label,
824 seb.stack_base.description,
825 seb.stack_base.is_collapsed as i32,
826 seb.stack_base.is_plot as i32,
827 seb.stack_base.is_visible as i32,
828 seb.stack_base.locked as i32,
829 seb.stack_base.styles.opacity,
830 seb.clip as i32,
831 seb.label_visible as i32,
832 ],
833 )?;
834 Ok(())
835}
836
837fn write_text_element(tx: &Transaction, e: &DucTextElement) -> SerializeResult<()> {
840 tx.execute(
841 "INSERT INTO element_text (
842 element_id, text, original_text, auto_resize, container_id,
843 is_ltr, font_family, big_font_family, text_align, vertical_align,
844 line_height, line_spacing_value, line_spacing_type,
845 oblique_angle, font_size, width_factor,
846 is_upside_down, is_backwards
847 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
848 params![
849 e.base.id,
850 e.text,
851 e.original_text,
852 e.auto_resize as i32,
853 e.container_id,
854 e.style.is_ltr as i32,
855 e.style.font_family,
856 e.style.big_font_family,
857 e.style.text_align as i32,
858 e.style.vertical_align as i32,
859 e.style.line_height,
860 e.style.line_spacing.value,
861 e.style.line_spacing.line_type.map(|t| t as i32),
862 e.style.oblique_angle,
863 e.style.font_size,
864 e.style.width_factor,
865 e.style.is_upside_down as i32,
866 e.style.is_backwards as i32,
867 ],
868 )?;
869 Ok(())
870}
871
872fn write_image_element(tx: &Transaction, e: &DucImageElement) -> SerializeResult<()> {
875 let scale_x = e.scale.first().copied().unwrap_or(1.0);
876 let scale_y = e.scale.get(1).copied().unwrap_or(1.0);
877 tx.execute(
878 "INSERT INTO element_image (
879 element_id, file_id, status, scale_x, scale_y,
880 crop_x, crop_y, crop_width, crop_height, crop_natural_width, crop_natural_height,
881 filter_brightness, filter_contrast
882 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
883 params![
884 e.base.id,
885 e.file_id,
886 e.status as i32,
887 scale_x,
888 scale_y,
889 e.crop.as_ref().map(|c| c.x),
890 e.crop.as_ref().map(|c| c.y),
891 e.crop.as_ref().map(|c| c.width),
892 e.crop.as_ref().map(|c| c.height),
893 e.crop.as_ref().map(|c| c.natural_width),
894 e.crop.as_ref().map(|c| c.natural_height),
895 e.filter.as_ref().map(|f| f.brightness),
896 e.filter.as_ref().map(|f| f.contrast),
897 ],
898 )?;
899 Ok(())
900}
901
902fn write_freedraw_element(tx: &Transaction, e: &DucFreeDrawElement) -> SerializeResult<()> {
905 let pressures_blob: Option<Vec<u8>> = if e.pressures.is_empty() {
906 None
907 } else {
908 Some(e.pressures.iter().flat_map(|p| p.to_le_bytes()).collect())
909 };
910
911 tx.execute(
912 "INSERT INTO element_freedraw (
913 element_id, size, thinning, smoothing, streamline, easing,
914 start_cap, start_taper, start_easing,
915 end_cap, end_taper, end_easing,
916 pressures, simulate_pressure,
917 last_committed_point_x, last_committed_point_y, last_committed_point_mirror,
918 svg_path
919 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
920 params![
921 e.base.id,
922 e.size,
923 e.thinning,
924 e.smoothing,
925 e.streamline,
926 e.easing,
927 e.start.as_ref().map(|s| s.cap as i32),
928 e.start.as_ref().map(|s| s.taper),
929 e.start.as_ref().map(|s| s.easing.clone()),
930 e.end.as_ref().map(|en| en.cap as i32),
931 e.end.as_ref().map(|en| en.taper),
932 e.end.as_ref().map(|en| en.easing.clone()),
933 pressures_blob,
934 e.simulate_pressure as i32,
935 e.last_committed_point.as_ref().map(|p| p.x),
936 e.last_committed_point.as_ref().map(|p| p.y),
937 e.last_committed_point.as_ref().and_then(|p| p.mirroring.map(|m| m as i32)),
938 e.svg_path,
939 ],
940 )?;
941
942 let mut stmt = tx.prepare_cached(
943 "INSERT INTO freedraw_element_points (element_id, sort_order, x, y, mirroring)
944 VALUES (?1, ?2, ?3, ?4, ?5)"
945 )?;
946 for (i, pt) in e.points.iter().enumerate() {
947 stmt.execute(params![e.base.id, i as i32, pt.x, pt.y, pt.mirroring.map(|m| m as i32)])?;
948 }
949
950 Ok(())
951}
952
953fn write_linear_element(
956 tx: &Transaction,
957 lb: &DucLinearElementBase,
958 wipeout_below: bool,
959 elbowed: bool,
960) -> SerializeResult<()> {
961 let id = &lb.base.id;
962
963 tx.execute(
964 "INSERT INTO element_linear (
965 element_id,
966 last_committed_point_x, last_committed_point_y, last_committed_point_mirror,
967 start_binding_element_id, start_binding_focus, start_binding_gap,
968 start_binding_fixed_point_x, start_binding_fixed_point_y,
969 start_binding_point_index, start_binding_point_offset,
970 start_binding_head_type, start_binding_head_block_id, start_binding_head_size,
971 end_binding_element_id, end_binding_focus, end_binding_gap,
972 end_binding_fixed_point_x, end_binding_fixed_point_y,
973 end_binding_point_index, end_binding_point_offset,
974 end_binding_head_type, end_binding_head_block_id, end_binding_head_size,
975 wipeout_below, elbowed
976 ) VALUES (
977 ?1,
978 ?2, ?3, ?4,
979 ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
980 ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24,
981 ?25, ?26
982 )",
983 params![
984 id,
985 lb.last_committed_point.as_ref().map(|p| p.x),
986 lb.last_committed_point.as_ref().map(|p| p.y),
987 lb.last_committed_point.as_ref().and_then(|p| p.mirroring.map(|m| m as i32)),
988 lb.start_binding.as_ref().map(|b| b.element_id.clone()),
989 lb.start_binding.as_ref().map(|b| b.focus),
990 lb.start_binding.as_ref().map(|b| b.gap),
991 lb.start_binding.as_ref().and_then(|b| b.fixed_point.as_ref().map(|fp| fp.x)),
992 lb.start_binding.as_ref().and_then(|b| b.fixed_point.as_ref().map(|fp| fp.y)),
993 lb.start_binding.as_ref().and_then(|b| b.point.as_ref().map(|p| p.index)),
994 lb.start_binding.as_ref().and_then(|b| b.point.as_ref().map(|p| p.offset)),
995 lb.start_binding.as_ref().and_then(|b| b.head.as_ref().and_then(|h| h.head_type.map(|t| t as i32))),
996 lb.start_binding.as_ref().and_then(|b| b.head.as_ref().and_then(|h| h.block_id.clone())),
997 lb.start_binding.as_ref().and_then(|b| b.head.as_ref().map(|h| h.size)),
998 lb.end_binding.as_ref().map(|b| b.element_id.clone()),
999 lb.end_binding.as_ref().map(|b| b.focus),
1000 lb.end_binding.as_ref().map(|b| b.gap),
1001 lb.end_binding.as_ref().and_then(|b| b.fixed_point.as_ref().map(|fp| fp.x)),
1002 lb.end_binding.as_ref().and_then(|b| b.fixed_point.as_ref().map(|fp| fp.y)),
1003 lb.end_binding.as_ref().and_then(|b| b.point.as_ref().map(|p| p.index)),
1004 lb.end_binding.as_ref().and_then(|b| b.point.as_ref().map(|p| p.offset)),
1005 lb.end_binding.as_ref().and_then(|b| b.head.as_ref().and_then(|h| h.head_type.map(|t| t as i32))),
1006 lb.end_binding.as_ref().and_then(|b| b.head.as_ref().and_then(|h| h.block_id.clone())),
1007 lb.end_binding.as_ref().and_then(|b| b.head.as_ref().map(|h| h.size)),
1008 wipeout_below as i32,
1009 elbowed as i32,
1010 ],
1011 )?;
1012
1013 {
1014 let mut stmt = tx.prepare_cached(
1015 "INSERT INTO linear_element_points (element_id, sort_order, x, y, mirroring)
1016 VALUES (?1, ?2, ?3, ?4, ?5)"
1017 )?;
1018 for (i, pt) in lb.points.iter().enumerate() {
1019 stmt.execute(params![id, i as i32, pt.x, pt.y, pt.mirroring.map(|m| m as i32)])?;
1020 }
1021 }
1022
1023 {
1024 let mut stmt = tx.prepare_cached(
1025 "INSERT INTO linear_element_lines (element_id, sort_order, start_index, start_handle_x, start_handle_y, end_index, end_handle_x, end_handle_y)
1026 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
1027 )?;
1028 for (i, line) in lb.lines.iter().enumerate() {
1029 stmt.execute(params![
1030 id,
1031 i as i32,
1032 line.start.index,
1033 line.start.handle.as_ref().map(|h| h.x),
1034 line.start.handle.as_ref().map(|h| h.y),
1035 line.end.index,
1036 line.end.handle.as_ref().map(|h| h.x),
1037 line.end.handle.as_ref().map(|h| h.y),
1038 ])?;
1039 }
1040 }
1041
1042 for (i, path) in lb.path_overrides.iter().enumerate() {
1043 let path_id: i64 = tx.query_row(
1044 "INSERT INTO linear_path_overrides (element_id, sort_order) VALUES (?1, ?2) RETURNING id",
1045 params![id, i as i32],
1046 |row| row.get(0),
1047 )?;
1048
1049 let mut idx_stmt = tx.prepare_cached(
1050 "INSERT INTO linear_path_override_indices (path_override_id, sort_order, line_index)
1051 VALUES (?1, ?2, ?3)"
1052 )?;
1053 for (j, &line_idx) in path.line_indices.iter().enumerate() {
1054 idx_stmt.execute(params![path_id, j as i32, line_idx])?;
1055 }
1056
1057 if let Some(ref bg) = path.background {
1058 write_background(tx, "path_override", &path_id.to_string(), 0, bg)?;
1059 }
1060 if let Some(ref st) = path.stroke {
1061 write_stroke(tx, "path_override", &path_id.to_string(), 0, st)?;
1062 }
1063 }
1064
1065 Ok(())
1066}
1067
1068fn write_document_grid_config(
1071 tx: &Transaction,
1072 element_id: &str,
1073 file_id: Option<&str>,
1074 gc: &DocumentGridConfig,
1075) -> SerializeResult<()> {
1076 tx.execute(
1077 "INSERT INTO document_grid_config
1078 (element_id, file_id, grid_columns, grid_gap_x, grid_gap_y, grid_first_page_alone, grid_scale)
1079 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1080 params![element_id, file_id, gc.columns, gc.gap_x, gc.gap_y, gc.first_page_alone as i32, gc.scale],
1081 )?;
1082 Ok(())
1083}
1084
1085fn write_model_element(tx: &Transaction, e: &DucModelElement) -> SerializeResult<()> {
1088 tx.execute(
1089 "INSERT INTO element_model (element_id, model_type, code, thumbnail)
1090 VALUES (?1, ?2, ?3, ?4)",
1091 params![e.base.id, e.model_type, e.code, e.thumbnail],
1092 )?;
1093
1094 write_element_file_ids(tx, &e.base.id, &e.file_ids)?;
1095
1096 if let Some(ref vs) = e.viewer_state {
1097 write_model_viewer_state(tx, "element", &e.base.id, vs)?;
1098 }
1099
1100 Ok(())
1101}
1102
1103fn write_element_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String]) -> SerializeResult<()> {
1104 let mut stmt = tx.prepare_cached(
1105 "INSERT OR IGNORE INTO model_element_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
1106 )?;
1107 for (i, fid) in file_ids.iter().enumerate() {
1108 stmt.execute(params![element_id, fid, i as i32])?;
1109 }
1110 Ok(())
1111}
1112
1113fn write_doc_referenced_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String]) -> SerializeResult<()> {
1114 let mut stmt = tx.prepare_cached(
1115 "INSERT OR IGNORE INTO doc_element_referenced_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
1116 )?;
1117 for (i, fid) in file_ids.iter().enumerate() {
1118 stmt.execute(params![element_id, fid, i as i32])?;
1119 }
1120 Ok(())
1121}
1122
1123fn write_model_viewer_state(
1124 tx: &Transaction,
1125 owner_type: &str,
1126 owner_id: &str,
1127 vs: &Viewer3DState,
1128) -> SerializeResult<()> {
1129 let cam = &vs.camera;
1130 let disp = &vs.display;
1131 let mat = &vs.material;
1132 let clip = &vs.clipping;
1133 let expl = &vs.explode;
1134 let zeb = &vs.zebra;
1135
1136 let (grid_uniform, grid_xy, grid_xz, grid_yz) = match &disp.grid {
1137 Viewer3DGrid::Uniform(v) => (Some(*v as i32), 0i32, 0i32, 0i32),
1138 Viewer3DGrid::PerPlane(p) => (None, p.xy as i32, p.xz as i32, p.yz as i32),
1139 };
1140
1141 tx.execute(
1142 "INSERT INTO model_viewer_state (
1143 owner_type, owner_id,
1144 camera_control, camera_ortho, camera_up,
1145 camera_position_x, camera_position_y, camera_position_z,
1146 camera_quaternion_x, camera_quaternion_y, camera_quaternion_z, camera_quaternion_w,
1147 camera_target_x, camera_target_y, camera_target_z,
1148 camera_zoom, camera_pan_speed, camera_rotate_speed, camera_zoom_speed, camera_holroyd,
1149 display_wireframe, display_transparent, display_black_edges,
1150 display_grid_uniform, display_grid_xy, display_grid_xz, display_grid_yz,
1151 display_axes_visible, display_axes_at_origin,
1152 material_metalness, material_roughness, material_default_opacity,
1153 material_edge_color, material_ambient_intensity, material_direct_intensity,
1154 clip_x_enabled, clip_x_value, clip_x_normal_x, clip_x_normal_y, clip_x_normal_z,
1155 clip_y_enabled, clip_y_value, clip_y_normal_x, clip_y_normal_y, clip_y_normal_z,
1156 clip_z_enabled, clip_z_value, clip_z_normal_x, clip_z_normal_y, clip_z_normal_z,
1157 clip_intersection, clip_show_planes, clip_object_color_caps,
1158 explode_active, explode_value,
1159 zebra_active, zebra_stripe_count, zebra_stripe_direction,
1160 zebra_color_scheme, zebra_opacity, zebra_mapping_mode
1161 ) VALUES (
1162 ?, ?,
1163 ?, ?, ?,
1164 ?, ?, ?,
1165 ?, ?, ?, ?,
1166 ?, ?, ?,
1167 ?, ?, ?, ?, ?,
1168 ?, ?, ?,
1169 ?, ?, ?, ?,
1170 ?, ?,
1171 ?, ?, ?,
1172 ?, ?, ?,
1173 ?, ?, ?, ?, ?,
1174 ?, ?, ?, ?, ?,
1175 ?, ?, ?, ?, ?,
1176 ?, ?, ?,
1177 ?, ?,
1178 ?, ?, ?,
1179 ?, ?, ?
1180 )",
1181 params![
1182 owner_type,
1183 owner_id,
1184 cam.control, cam.ortho as i32, cam.up,
1185 cam.position[0], cam.position[1], cam.position[2],
1186 cam.quaternion[0], cam.quaternion[1], cam.quaternion[2], cam.quaternion[3],
1187 cam.target[0], cam.target[1], cam.target[2],
1188 cam.zoom, cam.pan_speed, cam.rotate_speed, cam.zoom_speed, cam.holroyd as i32,
1189 disp.wireframe as i32, disp.transparent as i32, disp.black_edges as i32,
1190 grid_uniform, grid_xy, grid_xz, grid_yz,
1191 disp.axes_visible as i32, disp.axes_at_origin as i32,
1192 mat.metalness, mat.roughness, mat.default_opacity,
1193 mat.edge_color, mat.ambient_intensity, mat.direct_intensity,
1194 clip.x.enabled as i32, clip.x.value,
1195 clip.x.normal.as_ref().map(|n| n[0]),
1196 clip.x.normal.as_ref().map(|n| n[1]),
1197 clip.x.normal.as_ref().map(|n| n[2]),
1198 clip.y.enabled as i32, clip.y.value,
1199 clip.y.normal.as_ref().map(|n| n[0]),
1200 clip.y.normal.as_ref().map(|n| n[1]),
1201 clip.y.normal.as_ref().map(|n| n[2]),
1202 clip.z.enabled as i32, clip.z.value,
1203 clip.z.normal.as_ref().map(|n| n[0]),
1204 clip.z.normal.as_ref().map(|n| n[1]),
1205 clip.z.normal.as_ref().map(|n| n[2]),
1206 clip.intersection as i32, clip.show_planes as i32, clip.object_color_caps as i32,
1207 expl.active as i32, expl.value,
1208 zeb.active as i32, zeb.stripe_count, zeb.stripe_direction,
1209 zeb.color_scheme, zeb.opacity, zeb.mapping_mode,
1210 ],
1211 )?;
1212 Ok(())
1213}
1214
1215fn write_background(
1218 tx: &Transaction,
1219 owner_type: &str,
1220 owner_id: &str,
1221 sort_order: i32,
1222 bg: &ElementBackground,
1223) -> SerializeResult<()> {
1224 let c = &bg.content;
1225 tx.execute(
1226 "INSERT INTO backgrounds (
1227 owner_type, owner_id, sort_order,
1228 preference, src, visible, opacity,
1229 tiling_size_in_percent, tiling_angle, tiling_spacing, tiling_offset_x, tiling_offset_y,
1230 hatch_style, hatch_pattern_name, hatch_pattern_scale, hatch_pattern_angle,
1231 hatch_pattern_origin_x, hatch_pattern_origin_y, hatch_pattern_origin_mirror,
1232 hatch_pattern_double,
1233 hatch_custom_pattern_name, hatch_custom_pattern_desc,
1234 image_filter_brightness, image_filter_contrast
1235 ) VALUES (
1236 ?1, ?2, ?3, ?4, ?5, ?6, ?7,
1237 ?8, ?9, ?10, ?11, ?12,
1238 ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24
1239 )",
1240 params![
1241 owner_type, owner_id, sort_order,
1242 c.preference.map(|p| p as i32), c.src, c.visible as i32, c.opacity,
1243 c.tiling.as_ref().map(|t| t.size_in_percent),
1244 c.tiling.as_ref().map(|t| t.angle),
1245 c.tiling.as_ref().and_then(|t| t.spacing),
1246 c.tiling.as_ref().and_then(|t| t.offset_x),
1247 c.tiling.as_ref().and_then(|t| t.offset_y),
1248 c.hatch.as_ref().map(|h| h.hatch_style as i32),
1249 c.hatch.as_ref().map(|h| h.pattern_name.clone()),
1250 c.hatch.as_ref().map(|h| h.pattern_scale),
1251 c.hatch.as_ref().map(|h| h.pattern_angle),
1252 c.hatch.as_ref().map(|h| h.pattern_origin.x),
1253 c.hatch.as_ref().map(|h| h.pattern_origin.y),
1254 c.hatch.as_ref().and_then(|h| h.pattern_origin.mirroring.map(|m| m as i32)),
1255 c.hatch.as_ref().map(|h| h.pattern_double as i32),
1256 c.hatch.as_ref().and_then(|h| h.custom_pattern.as_ref().map(|cp| cp.name.clone())),
1257 c.hatch.as_ref().and_then(|h| h.custom_pattern.as_ref().and_then(|cp| cp.description.clone())),
1258 c.image_filter.as_ref().map(|f| f.brightness),
1259 c.image_filter.as_ref().map(|f| f.contrast),
1260 ],
1261 )?;
1262
1263 if let Some(ref hatch) = c.hatch {
1264 if let Some(ref cp) = hatch.custom_pattern {
1265 let bg_id: i64 = tx.query_row(
1266 "SELECT last_insert_rowid()", [], |row| row.get(0),
1267 )?;
1268 write_hatch_pattern_lines(tx, "background", bg_id, &cp.lines)?;
1269 }
1270 }
1271
1272 Ok(())
1273}
1274
1275fn write_stroke(
1276 tx: &Transaction,
1277 owner_type: &str,
1278 owner_id: &str,
1279 sort_order: i32,
1280 st: &ElementStroke,
1281) -> SerializeResult<()> {
1282 let c = &st.content;
1283 let dash_blob: Option<Vec<u8>> = st.style.dash.as_ref().map(|d| {
1284 d.iter().flat_map(|v| v.to_le_bytes()).collect()
1285 });
1286 let sides_blob: Option<Vec<u8>> = st.stroke_sides.as_ref().and_then(|s| {
1287 s.values.as_ref().map(|v| v.iter().flat_map(|val| val.to_le_bytes()).collect())
1288 });
1289
1290 tx.execute(
1291 "INSERT INTO strokes (
1292 owner_type, owner_id, sort_order,
1293 preference, src, visible, opacity,
1294 tiling_size_in_percent, tiling_angle, tiling_spacing, tiling_offset_x, tiling_offset_y,
1295 hatch_style, hatch_pattern_name, hatch_pattern_scale, hatch_pattern_angle,
1296 hatch_pattern_origin_x, hatch_pattern_origin_y, hatch_pattern_origin_mirror,
1297 hatch_pattern_double,
1298 hatch_custom_pattern_name, hatch_custom_pattern_desc,
1299 image_filter_brightness, image_filter_contrast,
1300 width,
1301 style_preference, style_cap, style_join, style_dash, style_dash_line_override, style_dash_cap, style_miter_limit,
1302 placement,
1303 sides_preference, sides_values
1304 ) VALUES (
1305 ?1, ?2, ?3,
1306 ?4, ?5, ?6, ?7,
1307 ?8, ?9, ?10, ?11, ?12,
1308 ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24,
1309 ?25,
1310 ?26, ?27, ?28, ?29, ?30, ?31, ?32,
1311 ?33,
1312 ?34, ?35
1313 )",
1314 params![
1315 owner_type, owner_id, sort_order,
1316 c.preference.map(|p| p as i32), c.src, c.visible as i32, c.opacity,
1317 c.tiling.as_ref().map(|t| t.size_in_percent),
1318 c.tiling.as_ref().map(|t| t.angle),
1319 c.tiling.as_ref().and_then(|t| t.spacing),
1320 c.tiling.as_ref().and_then(|t| t.offset_x),
1321 c.tiling.as_ref().and_then(|t| t.offset_y),
1322 c.hatch.as_ref().map(|h| h.hatch_style as i32),
1323 c.hatch.as_ref().map(|h| h.pattern_name.clone()),
1324 c.hatch.as_ref().map(|h| h.pattern_scale),
1325 c.hatch.as_ref().map(|h| h.pattern_angle),
1326 c.hatch.as_ref().map(|h| h.pattern_origin.x),
1327 c.hatch.as_ref().map(|h| h.pattern_origin.y),
1328 c.hatch.as_ref().and_then(|h| h.pattern_origin.mirroring.map(|m| m as i32)),
1329 c.hatch.as_ref().map(|h| h.pattern_double as i32),
1330 c.hatch.as_ref().and_then(|h| h.custom_pattern.as_ref().map(|cp| cp.name.clone())),
1331 c.hatch.as_ref().and_then(|h| h.custom_pattern.as_ref().and_then(|cp| cp.description.clone())),
1332 c.image_filter.as_ref().map(|f| f.brightness),
1333 c.image_filter.as_ref().map(|f| f.contrast),
1334 st.width,
1335 st.style.preference.map(|p| p as i32),
1336 st.style.cap.map(|p| p as i32),
1337 st.style.join.map(|p| p as i32),
1338 dash_blob,
1339 st.style.dash_line_override,
1340 st.style.dash_cap.map(|p| p as i32),
1341 st.style.miter_limit,
1342 st.placement.map(|p| p as i32),
1343 st.stroke_sides.as_ref().and_then(|s| s.preference.map(|p| p as i32)),
1344 sides_blob,
1345 ],
1346 )?;
1347
1348 if let Some(ref hatch) = c.hatch {
1349 if let Some(ref cp) = hatch.custom_pattern {
1350 let st_id: i64 = tx.query_row(
1351 "SELECT last_insert_rowid()", [], |row| row.get(0),
1352 )?;
1353 write_hatch_pattern_lines(tx, "stroke", st_id, &cp.lines)?;
1354 }
1355 }
1356
1357 Ok(())
1358}
1359
1360fn write_hatch_pattern_lines(
1361 tx: &Transaction,
1362 owner_type: &str,
1363 owner_id: i64,
1364 lines: &[HatchPatternLine],
1365) -> SerializeResult<()> {
1366 let mut stmt = tx.prepare_cached(
1367 "INSERT INTO hatch_pattern_lines (
1368 owner_type, owner_id, sort_order,
1369 angle, origin_x, origin_y, origin_mirroring,
1370 offset_x, offset_y, dash_pattern
1371 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"
1372 )?;
1373 for (i, line) in lines.iter().enumerate() {
1374 let offset_x = line.offset.first().copied().unwrap_or(0.0);
1375 let offset_y = line.offset.get(1).copied().unwrap_or(0.0);
1376 let dash_blob: Option<Vec<u8>> = if line.dash_pattern.is_empty() {
1377 None
1378 } else {
1379 Some(line.dash_pattern.iter().flat_map(|v| v.to_le_bytes()).collect())
1380 };
1381 stmt.execute(params![
1382 owner_type,
1383 owner_id,
1384 i as i32,
1385 line.angle,
1386 line.origin.x,
1387 line.origin.y,
1388 line.origin.mirroring.map(|m| m as i32),
1389 offset_x,
1390 offset_y,
1391 dash_blob,
1392 ])?;
1393 }
1394 Ok(())
1395}
1396
1397fn write_external_files(
1400 tx: &Transaction,
1401 files: &Option<std::collections::HashMap<String, DucExternalFile>>,
1402 files_data: &Option<std::collections::HashMap<String, serde_bytes::ByteBuf>>,
1403) -> SerializeResult<()> {
1404 let Some(files) = files else { return Ok(()) };
1405 let mut file_stmt = tx.prepare_cached(
1406 "INSERT OR REPLACE INTO external_files (id, active_revision_id, updated, version)
1407 VALUES (?1, ?2, ?3, ?4)"
1408 )?;
1409 let mut rev_stmt = tx.prepare_cached(
1410 "INSERT OR REPLACE INTO external_file_revisions
1411 (id, file_id, size_bytes, checksum, source_name, mime_type, message, created, last_retrieved)
1412 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"
1413 )?;
1414 let mut data_stmt = tx.prepare_cached(
1415 "INSERT OR REPLACE INTO external_file_revision_data (revision_id, data)
1416 VALUES (?1, ?2)"
1417 )?;
1418 for (_key, file) in files {
1419 file_stmt.execute(params![
1420 file.id,
1421 file.active_revision_id,
1422 file.updated,
1423 file.version,
1424 ])?;
1425 for (_rev_key, rev) in &file.revisions {
1426 rev_stmt.execute(params![
1427 rev.id,
1428 file.id,
1429 rev.size_bytes,
1430 rev.checksum,
1431 rev.source_name,
1432 rev.mime_type,
1433 rev.message,
1434 rev.created,
1435 rev.last_retrieved,
1436 ])?;
1437 }
1438 }
1439 if let Some(data_map) = files_data {
1441 for (rev_id, blob) in data_map {
1442 data_stmt.execute(params![rev_id, blob.as_ref()])?;
1443 }
1444 }
1445 Ok(())
1446}
1447
1448fn write_version_graph(tx: &Transaction, vg: &Option<VersionGraph>) -> SerializeResult<()> {
1451 let Some(vg) = vg else { return Ok(()) };
1452
1453 tx.execute(
1454 "UPDATE version_graph SET
1455 current_version = ?1,
1456 current_schema_version = ?2,
1457 user_checkpoint_version_id = ?3,
1458 latest_version_id = ?4,
1459 chain_count = ?5,
1460 total_size = ?6
1461 WHERE id = 1",
1462 params![
1463 vg.metadata.current_version,
1464 vg.metadata.current_schema_version,
1465 vg.user_checkpoint_version_id,
1466 vg.latest_version_id,
1467 vg.metadata.chain_count,
1468 vg.metadata.total_size,
1469 ],
1470 )?;
1471
1472 {
1473 let mut stmt = tx.prepare_cached(
1474 "INSERT OR REPLACE INTO version_chains
1475 (id, schema_version, start_version, end_version, migration_id, root_checkpoint_id)
1476 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
1477 )?;
1478 for chain in &vg.chains {
1479 let migration_id: Option<i64> = if let Some(ref mig) = chain.migration {
1480 let mid: i64 = tx.query_row(
1481 "INSERT INTO schema_migrations
1482 (from_schema_version, to_schema_version, migration_name, migration_checksum, applied_at, boundary_checkpoint_id)
1483 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1484 RETURNING id",
1485 params![
1486 mig.from_schema_version,
1487 mig.to_schema_version,
1488 mig.migration_name,
1489 mig.migration_checksum,
1490 mig.applied_at,
1491 mig.boundary_checkpoint_id,
1492 ],
1493 |row| row.get(0),
1494 )?;
1495 Some(mid)
1496 } else {
1497 None
1498 };
1499
1500 stmt.execute(params![
1501 chain.id,
1502 chain.schema_version,
1503 chain.start_version,
1504 chain.end_version,
1505 migration_id,
1506 chain.root_checkpoint_id,
1507 ])?;
1508 }
1509 }
1510
1511 {
1512 let mut cp_stmt = tx.prepare_cached(
1513 "INSERT OR REPLACE INTO checkpoints
1514 (id, parent_id, chain_id, version_number, schema_version, timestamp,
1515 description, is_manual_save, is_schema_boundary, user_id, data, size_bytes)
1516 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"
1517 )?;
1518 for cp in &vg.checkpoints {
1519 let chain_id = vg.chains.iter()
1520 .find(|c| c.schema_version == cp.schema_version)
1521 .map(|c| c.id.as_str())
1522 .unwrap_or("");
1523 cp_stmt.execute(params![
1524 cp.base.id,
1525 cp.base.parent_id,
1526 chain_id,
1527 cp.version_number,
1528 cp.schema_version,
1529 cp.base.timestamp,
1530 cp.base.description,
1531 cp.base.is_manual_save as i32,
1532 cp.is_schema_boundary as i32,
1533 cp.base.user_id,
1534 cp.data,
1535 cp.size_bytes,
1536 ])?;
1537 }
1538 }
1539
1540 {
1541 let mut d_stmt = tx.prepare_cached(
1542 "INSERT OR REPLACE INTO deltas
1543 (id, parent_id, base_checkpoint_id, chain_id, delta_sequence, version_number,
1544 schema_version, timestamp, description, is_manual_save, user_id, changeset, size_bytes)
1545 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)"
1546 )?;
1547 for (i, delta) in vg.deltas.iter().enumerate() {
1548 let chain_id = vg.chains.iter()
1549 .find(|c| c.schema_version == delta.schema_version)
1550 .map(|c| c.id.as_str())
1551 .unwrap_or("");
1552 d_stmt.execute(params![
1553 delta.base.id,
1554 delta.base.parent_id,
1555 delta.base_checkpoint_id,
1556 chain_id,
1557 (i + 1) as i64,
1558 delta.version_number,
1559 delta.schema_version,
1560 delta.base.timestamp,
1561 delta.base.description,
1562 delta.base.is_manual_save as i32,
1563 delta.base.user_id,
1564 delta.payload,
1565 delta.size_bytes,
1566 ])?;
1567 }
1568 }
1569
1570 Ok(())
1571}