1use crate::dialect::SqliteDialect;
4use openehr::base::{HierObjectId, ObjectId, ObjectRef, ObjectVersionId};
5use openehr::rm::common::{CommitError, Contribution, Version};
6use openehr::rm::data_types::DvDateTime;
7use openehr::rm::ehr::{Composition, Ehr};
8use openehr::validation::Validate as _;
9use openehr_store::record::{CompositionIndexRow, StoredInstant, VersionRow};
10use openehr_store::{CommitOutcome, Result, Store, StoreError, ddl_script};
11use rusqlite::{Connection, OptionalExtension as _, params};
12
13const ENGINE: &str = "SQLite";
15
16pub struct SqliteStore {
26 connection: Connection,
27}
28
29impl SqliteStore {
30 pub fn in_memory() -> Result<Self> {
36 Self::from_connection(Connection::open_in_memory().map_err(|e| engine(&e))?)
37 }
38
39 pub fn open(path: &std::path::Path) -> Result<Self> {
45 Self::from_connection(Connection::open(path).map_err(|e| engine(&e))?)
46 }
47
48 pub fn from_connection(connection: Connection) -> Result<Self> {
54 connection
55 .execute_batch("PRAGMA foreign_keys = ON;")
56 .map_err(|e| engine(&e))?;
57 Ok(Self { connection })
58 }
59
60 #[must_use]
63 pub fn connection(&self) -> &Connection {
64 &self.connection
65 }
66
67 fn read_version(row: &rusqlite::Row<'_>) -> rusqlite::Result<VersionRow> {
69 Ok(VersionRow {
70 uid: row.get("uid")?,
71 versioned_object_uid: row.get("versioned_object_uid")?,
72 creating_system_id: row.get("creating_system_id")?,
73 trunk_version: row.get("trunk_version")?,
74 branch_number: row.get("branch_number")?,
75 branch_version: row.get("branch_version")?,
76 preceding_version_uid: row.get("preceding_version_uid")?,
77 lifecycle_state_code: row.get("lifecycle_state_code")?,
78 is_deleted: row.get::<_, i64>("is_deleted")? != 0,
79 contribution_uid: row.get("contribution_uid")?,
80 audit_system_id: row.get("audit_system_id")?,
81 audit_change_type_code: row.get("audit_change_type_code")?,
82 audit_committer_name: row.get("audit_committer_name")?,
83 audit_time_committed: StoredInstant {
84 text: row.get("audit_time_committed_text")?,
85 utc_seconds: row.get("audit_time_committed_utc")?,
86 },
87 data_json: row.get("data_json")?,
88 })
89 }
90
91 const VERSION_COLUMNS: &'static str = "uid, versioned_object_uid, creating_system_id, \
94 trunk_version, branch_number, branch_version, preceding_version_uid, \
95 lifecycle_state_code, is_deleted, contribution_uid, audit_system_id, \
96 audit_change_type_code, audit_committer_name, audit_time_committed_text, \
97 audit_time_committed_utc, data_json";
98}
99
100fn engine(error: &rusqlite::Error) -> StoreError {
102 StoreError::Engine {
103 engine: ENGINE,
104 message: error.to_string(),
109 }
110}
111
112impl Store for SqliteStore {
113 fn engine(&self) -> &'static str {
114 ENGINE
115 }
116
117 fn install(&mut self) -> Result<()> {
118 self.connection
119 .execute_batch(&ddl_script(&SqliteDialect))
120 .map_err(|e| engine(&e))
121 }
122
123 fn create_ehr(&mut self, ehr: &Ehr) -> Result<()> {
124 let id = ehr.ehr_id().to_string();
125 let existing: Option<String> = self
126 .connection
127 .query_row(
128 "SELECT ehr_id FROM openehr_ehr WHERE ehr_id = ?1",
129 params![id],
130 |row| row.get(0),
131 )
132 .optional()
133 .map_err(|e| engine(&e))?;
134 if existing.is_some() {
135 return Err(StoreError::Conflict { kind: "ehr", id });
136 }
137 let created = StoredInstant::from_date_time(ehr.time_created().value());
138 self.connection
139 .execute(
140 "INSERT INTO openehr_ehr \
141 (ehr_id, system_id, time_created_text, time_created_utc, ehr_status_uid, ehr_access_uid) \
142 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
143 params![
144 id,
145 ehr.system_id().to_string(),
146 created.text,
147 created.utc_seconds,
148 ehr.ehr_status().id().to_string(),
149 ehr.ehr_access().id().to_string(),
150 ],
151 )
152 .map_err(|e| engine(&e))?;
153 Ok(())
154 }
155
156 fn get_ehr(&self, ehr_id: &HierObjectId) -> Result<Ehr> {
157 let id = ehr_id.to_string();
158 let row: Option<(String, String, String, String)> = self
159 .connection
160 .query_row(
161 "SELECT system_id, time_created_text, ehr_status_uid, ehr_access_uid \
162 FROM openehr_ehr WHERE ehr_id = ?1",
163 params![id],
164 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
165 )
166 .optional()
167 .map_err(|e| engine(&e))?;
168 let Some((system_id, created, status_uid, access_uid)) = row else {
169 return Err(StoreError::NotFound { kind: "ehr", id });
170 };
171 let reference = |uid: &str, ty: &'static str| -> Result<ObjectRef> {
172 Ok(ObjectRef::new(
173 "local",
174 ty,
175 ObjectId::HierObjectId(uid.parse()?),
176 )?)
177 };
178 Ok(Ehr::new(
179 system_id.parse()?,
180 ehr_id.clone(),
181 reference(&status_uid, "VERSIONED_EHR_STATUS")?,
182 reference(&access_uid, "VERSIONED_EHR_ACCESS")?,
183 DvDateTime::new(&created)?,
184 ))
185 }
186
187 fn create_contribution(
188 &mut self,
189 ehr_id: &HierObjectId,
190 contribution: &Contribution,
191 ) -> Result<()> {
192 let uid = contribution.uid().to_string();
193 let audit = contribution.audit();
194 let committed = StoredInstant::from_date_time(audit.time_committed().value());
195 self.connection
196 .execute(
197 "INSERT INTO openehr_contribution \
198 (uid, ehr_id, audit_change_type_code, audit_system_id, audit_committer_name, \
199 audit_time_committed_text, audit_time_committed_utc) \
200 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
201 params![
202 uid,
203 ehr_id.to_string(),
204 audit.change_type_code(),
205 audit.system_id(),
206 audit.committer().name(),
207 committed.text,
208 committed.utc_seconds,
209 ],
210 )
211 .map_err(|e| match e {
212 rusqlite::Error::SqliteFailure(f, _)
213 if f.code == rusqlite::ErrorCode::ConstraintViolation =>
214 {
215 StoreError::Conflict {
216 kind: "contribution",
217 id: uid.clone(),
218 }
219 }
220 ref other => engine(other),
221 })?;
222 Ok(())
223 }
224
225 #[allow(clippy::too_many_lines)]
229 fn commit_composition(
230 &mut self,
231 ehr_id: &HierObjectId,
232 version: &Version<Composition>,
233 contribution_uid: &str,
234 ) -> Result<CommitOutcome> {
235 if let Some(composition) = version.data() {
239 composition.validate_ok()?;
240 }
241
242 self.get_ehr(ehr_id)?;
246
247 let container_uid = version.uid().object_id().to_string();
248 let head: Option<(String, i64)> = self
249 .connection
250 .query_row(
251 "SELECT uid, trunk_version FROM openehr_version \
252 WHERE versioned_object_uid = ?1 \
253 ORDER BY trunk_version DESC, branch_number DESC, branch_version DESC LIMIT 1",
254 params![container_uid],
255 |row| Ok((row.get(0)?, row.get(1)?)),
256 )
257 .optional()
258 .map_err(|e| engine(&e))?;
259
260 let uid = version.uid().to_string();
264 let already: Option<String> = self
265 .connection
266 .query_row(
267 "SELECT uid FROM openehr_version WHERE uid = ?1",
268 params![uid],
269 |row| row.get(0),
270 )
271 .optional()
272 .map_err(|e| engine(&e))?;
273 if already.is_some() {
274 return Err(StoreError::Commit(CommitError::DuplicateVersion));
275 }
276 match (&head, version.preceding_version_uid()) {
277 (None, None) => {}
278 (None, Some(_)) | (Some(_), None) => {
279 return Err(StoreError::Commit(CommitError::PrecedingVersionMismatch));
280 }
281 (Some((latest, _)), Some(preceding)) => {
282 if latest != &preceding.to_string() {
283 return Err(StoreError::Commit(CommitError::NotLatest));
284 }
285 }
286 }
287
288 let row = VersionRow::project(version, contribution_uid)?;
289 let created_container = head.is_none();
290 let transaction = self
291 .connection
292 .unchecked_transaction()
293 .map_err(|e| engine(&e))?;
294
295 if created_container {
296 let created =
297 StoredInstant::from_date_time(version.commit_audit().time_committed().value());
298 transaction
299 .execute(
300 "INSERT INTO openehr_versioned_object \
301 (uid, ehr_id, rm_type, time_created_text, time_created_utc) \
302 VALUES (?1, ?2, ?3, ?4, ?5)",
303 params![
304 container_uid,
305 ehr_id.to_string(),
306 "COMPOSITION",
307 created.text,
308 created.utc_seconds
309 ],
310 )
311 .map_err(|e| engine(&e))?;
312 }
313
314 transaction
315 .execute(
316 "INSERT INTO openehr_version \
317 (uid, versioned_object_uid, creating_system_id, trunk_version, branch_number, \
318 branch_version, preceding_version_uid, lifecycle_state_code, is_deleted, \
319 contribution_uid, audit_system_id, audit_change_type_code, \
320 audit_committer_name, audit_time_committed_text, audit_time_committed_utc, \
321 data_json) \
322 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
323 params![
324 row.uid,
325 row.versioned_object_uid,
326 row.creating_system_id,
327 row.trunk_version,
328 row.branch_number,
329 row.branch_version,
330 row.preceding_version_uid,
331 row.lifecycle_state_code,
332 i64::from(row.is_deleted),
333 row.contribution_uid,
334 row.audit_system_id,
335 row.audit_change_type_code,
336 row.audit_committer_name,
337 row.audit_time_committed.text,
338 row.audit_time_committed.utc_seconds,
339 row.data_json,
340 ],
341 )
342 .map_err(|e| engine(&e))?;
343
344 if let Some(composition) = version.data() {
345 let index = CompositionIndexRow::project(&row.uid, &ehr_id.to_string(), composition)?;
346 transaction
347 .execute(
348 "INSERT INTO openehr_composition_index \
349 (version_uid, ehr_id, archetype_id, template_id, category_code, \
350 composer_name, language_code, territory_code, setting_code, \
351 context_start_text, context_start_utc, context_end_text, context_end_utc) \
352 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
353 params![
354 index.version_uid,
355 index.ehr_id,
356 index.archetype_id,
357 index.template_id,
358 index.category_code,
359 index.composer_name,
360 index.language_code,
361 index.territory_code,
362 index.setting_code,
363 index.context_start.as_ref().map(|i| i.text.clone()),
364 index.context_start.as_ref().and_then(|i| i.utc_seconds),
365 index.context_end.as_ref().map(|i| i.text.clone()),
366 index.context_end.as_ref().and_then(|i| i.utc_seconds),
367 ],
368 )
369 .map_err(|e| engine(&e))?;
370 }
371
372 transaction.commit().map_err(|e| engine(&e))?;
373 Ok(CommitOutcome {
374 version_uid: version.uid().clone(),
375 created_container,
376 })
377 }
378
379 fn get_version(&self, uid: &ObjectVersionId) -> Result<VersionRow> {
380 let id = uid.to_string();
381 self.connection
382 .query_row(
383 &format!(
384 "SELECT {} FROM openehr_version WHERE uid = ?1",
385 Self::VERSION_COLUMNS
386 ),
387 params![id],
388 Self::read_version,
389 )
390 .optional()
391 .map_err(|e| engine(&e))?
392 .ok_or(StoreError::NotFound {
393 kind: "version",
394 id,
395 })
396 }
397
398 fn latest_version(&self, versioned_object_uid: &HierObjectId) -> Result<VersionRow> {
399 let id = versioned_object_uid.to_string();
400 self.connection
401 .query_row(
402 &format!(
403 "SELECT {} FROM openehr_version WHERE versioned_object_uid = ?1 \
404 ORDER BY trunk_version DESC, branch_number DESC, branch_version DESC LIMIT 1",
405 Self::VERSION_COLUMNS
406 ),
407 params![id],
408 Self::read_version,
409 )
410 .optional()
411 .map_err(|e| engine(&e))?
412 .ok_or(StoreError::NotFound {
413 kind: "versioned_object",
414 id,
415 })
416 }
417
418 fn version_at_time(
419 &self,
420 versioned_object_uid: &HierObjectId,
421 at: &DvDateTime,
422 ) -> Result<VersionRow> {
423 let id = versioned_object_uid.to_string();
424 let Some(at_seconds) = StoredInstant::from_date_time(at.value()).utc_seconds else {
425 return Err(StoreError::NotFound {
430 kind: "version",
431 id,
432 });
433 };
434 self.connection
439 .query_row(
440 &format!(
441 "SELECT {} FROM openehr_version \
442 WHERE versioned_object_uid = ?1 \
443 AND audit_time_committed_utc IS NOT NULL \
444 AND audit_time_committed_utc <= ?2 \
445 ORDER BY audit_time_committed_utc DESC, trunk_version DESC LIMIT 1",
446 Self::VERSION_COLUMNS
447 ),
448 params![id, at_seconds],
449 Self::read_version,
450 )
451 .optional()
452 .map_err(|e| engine(&e))?
453 .ok_or(StoreError::NotFound {
454 kind: "version",
455 id,
456 })
457 }
458
459 fn all_versions(&self, versioned_object_uid: &HierObjectId) -> Result<Vec<VersionRow>> {
460 let mut statement = self
461 .connection
462 .prepare(&format!(
463 "SELECT {} FROM openehr_version WHERE versioned_object_uid = ?1 \
464 ORDER BY trunk_version ASC, branch_number ASC, branch_version ASC",
465 Self::VERSION_COLUMNS
466 ))
467 .map_err(|e| engine(&e))?;
468 let rows = statement
469 .query_map(
470 params![versioned_object_uid.to_string()],
471 Self::read_version,
472 )
473 .map_err(|e| engine(&e))?;
474 rows.collect::<rusqlite::Result<Vec<_>>>()
475 .map_err(|e| engine(&e))
476 }
477
478 fn find_compositions_by_archetype(
479 &self,
480 ehr_id: &HierObjectId,
481 archetype_id: &str,
482 ) -> Result<Vec<CompositionIndexRow>> {
483 let mut statement = self
484 .connection
485 .prepare(
486 "SELECT version_uid, ehr_id, archetype_id, template_id, category_code, \
487 composer_name, language_code, territory_code, setting_code, \
488 context_start_text, context_start_utc, context_end_text, context_end_utc \
489 FROM openehr_composition_index \
490 WHERE ehr_id = ?1 AND archetype_id = ?2 \
491 ORDER BY version_uid",
492 )
493 .map_err(|e| engine(&e))?;
494 let rows = statement
495 .query_map(params![ehr_id.to_string(), archetype_id], |row| {
496 let instant = |text: Option<String>, utc: Option<i64>| {
497 text.map(|text| StoredInstant {
498 text,
499 utc_seconds: utc,
500 })
501 };
502 Ok(CompositionIndexRow {
503 version_uid: row.get("version_uid")?,
504 ehr_id: row.get("ehr_id")?,
505 archetype_id: row.get("archetype_id")?,
506 template_id: row.get("template_id")?,
507 category_code: row.get("category_code")?,
508 composer_name: row.get("composer_name")?,
509 language_code: row.get("language_code")?,
510 territory_code: row.get("territory_code")?,
511 setting_code: row.get("setting_code")?,
512 context_start: instant(
513 row.get("context_start_text")?,
514 row.get("context_start_utc")?,
515 ),
516 context_end: instant(row.get("context_end_text")?, row.get("context_end_utc")?),
517 })
518 })
519 .map_err(|e| engine(&e))?;
520 rows.collect::<rusqlite::Result<Vec<_>>>()
521 .map_err(|e| engine(&e))
522 }
523}