1use super::{put_u32, put_u32s, put_u64, sha256, IndexSource, Input};
2use anyhow::{bail, ensure, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashSet};
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::Path;
8use std::sync::OnceLock;
9
10const MAGIC: &[u8; 8] = b"SNMEM003";
12const SORTED_IDS: u32 = 7;
14
15pub const METADATA: [&str; 4] = [
22 "effectiveTime",
23 "active",
24 "moduleId",
25 "referencedComponentId",
26];
27pub const ACTIVE: usize = 1;
29pub const REFERENCES: usize = 3;
31pub const FIELDS: usize = 4;
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35pub struct MemberManifest {
36 pub refset: u64,
37 pub bytes: u64,
38 pub sha256: String,
39 pub rows: usize,
40 pub fields: Vec<String>,
41}
42
43#[derive(Clone, Debug)]
44pub struct TextColumn {
45 pub offsets: Vec<u32>,
46 pub text: String,
47}
48impl Default for TextColumn {
49 fn default() -> Self {
50 Self {
51 offsets: vec![0],
52 text: String::new(),
53 }
54 }
55}
56impl TextColumn {
57 pub fn push(&mut self, value: &str) -> Result<()> {
58 if self.offsets.is_empty() {
59 self.offsets.push(0);
60 }
61 self.text.push_str(value);
62 self.offsets.push(u32::try_from(self.text.len())?);
63 Ok(())
64 }
65 pub fn get(&self, row: usize) -> &str {
66 &self.text[self.offsets[row] as usize..self.offsets[row + 1] as usize]
67 }
68}
69
70#[derive(Clone, Debug)]
71pub enum MemberColumn {
72 Id(Vec<u64>),
73 Integer(Vec<i64>),
74 Number(TextColumn),
75 Boolean(Vec<u8>),
76 Time(Vec<u32>),
77 Text(TextColumn),
78 Uuid(Vec<[u8; 16]>),
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
82#[serde(tag = "type", content = "value", rename_all = "snake_case")]
83pub enum MemberValue {
84 Concept(String),
85 Component(String),
87 Number(String),
88 Boolean(bool),
89 Time(String),
90 String(String),
91}
92
93pub fn is_concept_id(id: u64) -> bool {
95 matches!((id / 10) % 100, 0 | 10)
96}
97
98impl MemberColumn {
99 pub fn len(&self) -> usize {
100 match self {
101 Self::Id(v) => v.len(),
102 Self::Integer(v) => v.len(),
103 Self::Boolean(v) => v.len(),
104 Self::Time(v) => v.len(),
105 Self::Text(v) | Self::Number(v) => v.offsets.len().saturating_sub(1),
106 Self::Uuid(v) => v.len(),
107 }
108 }
109 pub fn is_empty(&self) -> bool {
110 self.len() == 0
111 }
112 fn select(&self, order: &[usize]) -> Result<Self> {
114 fn pick<T: Copy>(values: &[T], order: &[usize]) -> Vec<T> {
115 order.iter().map(|&row| values[row]).collect()
116 }
117 let text = |v: &TextColumn| -> Result<TextColumn> {
118 let mut out = TextColumn::default();
119 for &row in order {
120 out.push(v.get(row))?;
121 }
122 Ok(out)
123 };
124 Ok(match self {
125 Self::Id(v) => Self::Id(pick(v, order)),
126 Self::Integer(v) => Self::Integer(pick(v, order)),
127 Self::Number(v) => Self::Number(text(v)?),
128 Self::Boolean(v) => Self::Boolean(pick(v, order)),
129 Self::Time(v) => Self::Time(pick(v, order)),
130 Self::Text(v) => Self::Text(text(v)?),
131 Self::Uuid(v) => Self::Uuid(pick(v, order)),
132 })
133 }
134 pub fn value(&self, row: usize) -> MemberValue {
135 match self {
136 Self::Id(v) if is_concept_id(v[row]) => MemberValue::Concept(v[row].to_string()),
137 Self::Id(v) => MemberValue::Component(v[row].to_string()),
138 Self::Integer(v) => MemberValue::Number(v[row].to_string()),
139 Self::Number(v) => MemberValue::Number(v.get(row).into()),
140 Self::Boolean(v) => MemberValue::Boolean(v[row] != 0),
141 Self::Time(v) => MemberValue::Time(if v[row] == 0 {
142 String::new()
143 } else {
144 format!("{:08}", v[row])
145 }),
146 Self::Text(v) => MemberValue::String(v.get(row).into()),
147 Self::Uuid(v) => MemberValue::String(format_uuid(&v[row])),
148 }
149 }
150}
151
152pub fn parse_uuid(text: &str) -> Result<[u8; 16]> {
153 ensure!(
154 text.len() == 36 && [8, 13, 18, 23].iter().all(|&i| text.as_bytes()[i] == b'-'),
155 "Invalid member UUID"
156 );
157 let hex: String = text.chars().filter(|&c| c != '-').collect();
158 ensure!(
159 hex.len() == 32 && hex.bytes().all(|b| b.is_ascii_hexdigit()),
160 "Invalid member UUID"
161 );
162 let mut uuid = [0; 16];
163 for (i, byte) in uuid.iter_mut().enumerate() {
164 *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)?;
165 }
166 Ok(uuid)
167}
168pub fn format_uuid(uuid: &[u8; 16]) -> String {
169 let mut text = String::with_capacity(36);
170 for (i, b) in uuid.iter().enumerate() {
171 if [4, 6, 8, 10].contains(&i) {
172 text.push('-');
173 }
174 use std::fmt::Write;
175 write!(text, "{b:02x}").unwrap();
176 }
177 text
178}
179
180#[derive(Clone, Debug)]
182pub struct MemberTable {
183 pub refset: u64,
184 pub names: Vec<String>,
185 pub columns: Vec<MemberColumn>,
186}
187impl MemberTable {
188 pub fn append(&mut self, other: Self) -> Result<()> {
189 self.validate()?;
190 other.validate()?;
191 ensure!(
192 self.refset == other.refset && self.names == other.names,
193 "Refset schemas differ"
194 );
195 for (left, right) in self.columns.iter_mut().zip(other.columns) {
196 if let (MemberColumn::Integer(a), MemberColumn::Number(_)) = (&*left, &right) {
198 let mut text = TextColumn::default();
199 for value in a {
200 text.push(&value.to_string())?;
201 }
202 *left = MemberColumn::Number(text);
203 }
204 let right = match (&*left, right) {
205 (MemberColumn::Number(_), MemberColumn::Integer(b)) => {
206 let mut text = TextColumn::default();
207 for value in b {
208 text.push(&value.to_string())?;
209 }
210 MemberColumn::Number(text)
211 }
212 (_, right) => right,
213 };
214 match (left, right) {
215 (MemberColumn::Id(a), MemberColumn::Id(b)) => a.extend(b),
216 (MemberColumn::Integer(a), MemberColumn::Integer(b)) => a.extend(b),
217 (MemberColumn::Boolean(a), MemberColumn::Boolean(b)) => a.extend(b),
218 (MemberColumn::Time(a), MemberColumn::Time(b)) => a.extend(b),
219 (MemberColumn::Uuid(a), MemberColumn::Uuid(b)) => a.extend(b),
220 (MemberColumn::Text(a), MemberColumn::Text(b))
221 | (MemberColumn::Number(a), MemberColumn::Number(b)) => {
222 let start = u32::try_from(a.text.len())?;
223 for offset in b.offsets.into_iter().skip(1) {
224 a.offsets.push(
225 start.checked_add(offset).ok_or_else(|| {
226 anyhow::anyhow!("Member text exceeds u32 capacity")
227 })?,
228 );
229 }
230 a.text.push_str(&b.text);
231 }
232 _ => bail!("Refset field types differ"),
233 }
234 }
235 self.validate()
236 }
237 pub fn len(&self) -> usize {
238 self.columns.first().map_or(0, MemberColumn::len)
239 }
240 pub fn is_empty(&self) -> bool {
241 self.len() == 0
242 }
243 pub fn column(&self, name: &str) -> Option<&MemberColumn> {
244 self.names
245 .iter()
246 .position(|n| n.eq_ignore_ascii_case(name))
247 .map(|i| &self.columns[i])
248 }
249 pub fn validate(&self) -> Result<()> {
250 ensure!(
251 self.names.len() == self.columns.len()
252 && self.names.len() >= FIELDS
253 && self.names.len() <= 64,
254 "Invalid member schema"
255 );
256 ensure!(
257 self.names[..FIELDS] == METADATA,
258 "Invalid member metadata fields"
259 );
260 let mut names = HashSet::new();
261 for (name, col) in self.names.iter().zip(&self.columns) {
262 ensure!(
263 !name.is_empty()
264 && name.bytes().all(|b| b.is_ascii_alphabetic())
265 && names.insert(name.to_ascii_lowercase()),
266 "Invalid or duplicate member field"
267 );
268 ensure!(col.len() == self.len(), "Member column lengths differ");
269 match col {
270 MemberColumn::Id(v) => ensure!(
271 v.iter()
272 .all(|id| *id > 0 && *id < 1_000_000_000_000_000_000),
273 "Invalid member SCTID"
274 ),
275 MemberColumn::Boolean(v) => {
276 ensure!(v.iter().all(|&v| v <= 1), "Invalid member Boolean")
277 }
278 MemberColumn::Time(v) => {
279 ensure!(v.iter().all(|&v| valid_time(v)), "Invalid member date")
280 }
281 MemberColumn::Text(v) | MemberColumn::Number(v) => {
282 ensure!(
283 v.offsets.first() == Some(&0)
284 && v.offsets.last().copied() == u32::try_from(v.text.len()).ok(),
285 "Invalid member text offsets"
286 );
287 ensure!(
288 v.offsets.windows(2).all(|w| w[0] <= w[1])
289 && v.offsets
290 .iter()
291 .all(|&i| v.text.is_char_boundary(i as usize)),
292 "Invalid member UTF-8 boundary"
293 );
294 if matches!(col, MemberColumn::Number(_)) {
295 ensure!(
296 (0..v.offsets.len() - 1)
297 .all(|i| crate::decimal::Decimal::parse(v.get(i)).is_some()),
298 "Invalid numeric member field"
299 );
300 }
301 }
302 _ => {}
303 }
304 }
305 ensure!(
306 matches!(&self.columns[0], MemberColumn::Time(_))
307 && matches!(&self.columns[ACTIVE], MemberColumn::Boolean(_)),
308 "Invalid member status metadata"
309 );
310 ensure!(
311 matches!(&self.columns[2], MemberColumn::Id(_)),
312 "Invalid member module metadata"
313 );
314 ensure!(
315 matches!(&self.columns[REFERENCES], MemberColumn::Id(v) if v.iter().all(|id| matches!((id / 10) % 100, 0 | 10))),
316 "Refset does not reference concepts"
317 );
318 Ok(())
319 }
320 pub fn write(&self, directory: &Path) -> Result<MemberManifest> {
326 self.validate()?;
327 let MemberColumn::Id(references) = &self.columns[REFERENCES] else {
328 bail!("Invalid member reference column");
329 };
330 let mut order: Vec<usize> = (0..self.len()).collect();
331 order.sort_by_key(|&row| references[row]);
332 std::fs::create_dir_all(directory.join("members"))?;
333 let path = directory
334 .join("members")
335 .join(format!("{}.bin", self.refset));
336 let mut out = BufWriter::new(File::create_new(&path)?);
337 out.write_all(MAGIC)?;
338 put_u64(&mut out, self.refset)?;
339 let schema = serde_json::to_vec(&self.names)?;
340 put_u64(&mut out, schema.len() as u64)?;
341 out.write_all(&schema)?;
342 for (index, column) in self.columns.iter().enumerate() {
343 let column = &column.select(&order)?;
344 match column {
345 MemberColumn::Id(v) if index == REFERENCES => {
346 let bytes = super::varint::encode_u64(v)?;
347 put_u32(&mut out, SORTED_IDS)?;
348 put_u64(&mut out, v.len() as u64)?;
349 put_u64(&mut out, bytes.len() as u64)?;
350 out.write_all(&bytes)?;
351 }
352 MemberColumn::Id(v) => {
353 put_u32(&mut out, 0)?;
354 put_u64(&mut out, v.len() as u64)?;
355 for &n in v {
356 put_u64(&mut out, n)?;
357 }
358 }
359 MemberColumn::Integer(v) => {
360 put_u32(&mut out, 1)?;
361 put_u64(&mut out, v.len() as u64)?;
362 for &n in v {
363 put_u64(&mut out, n as u64)?;
364 }
365 }
366 MemberColumn::Boolean(v) => {
367 put_u32(&mut out, 2)?;
368 put_u64(&mut out, v.len() as u64)?;
369 out.write_all(v)?;
370 }
371 MemberColumn::Time(v) => {
372 put_u32(&mut out, 3)?;
373 put_u32s(&mut out, v)?;
374 }
375 MemberColumn::Text(v) | MemberColumn::Number(v) => {
376 put_u32(
377 &mut out,
378 if matches!(column, MemberColumn::Number(_)) {
379 6
380 } else {
381 4
382 },
383 )?;
384 put_u32s(&mut out, &v.offsets)?;
385 put_u64(&mut out, v.text.len() as u64)?;
386 out.write_all(v.text.as_bytes())?;
387 }
388 MemberColumn::Uuid(v) => {
389 put_u32(&mut out, 5)?;
390 put_u64(&mut out, v.len() as u64)?;
391 for uuid in v {
392 out.write_all(uuid)?;
393 }
394 }
395 }
396 }
397 out.flush()?;
398 out.get_ref().sync_all()?;
399 Ok(MemberManifest {
400 refset: self.refset,
401 bytes: path.metadata()?.len(),
402 sha256: sha256(&path)?,
403 rows: self.len(),
404 fields: self.names.clone(),
405 })
406 }
407 pub(super) fn open(source: &IndexSource, metadata: &MemberManifest) -> Result<Self> {
408 let mut input = Input::open(
409 &source.section(&format!("members/{}.bin", metadata.refset))?,
410 MAGIC,
411 )?;
412 let refset = input.u64()?;
413 let names: Vec<String> = serde_json::from_slice(&input.bytes()?)?;
414 ensure!(
415 names.len() <= 64 && names == metadata.fields,
416 "Member schema differs from manifest"
417 );
418 let mut columns = Vec::new();
419 for _ in &names {
420 columns.push(match input.u32()? {
421 0 => MemberColumn::Id({
422 let n = input.count(8)?;
423 (0..n).map(|_| input.u64()).collect::<Result<_>>()?
424 }),
425 1 => MemberColumn::Integer({
426 let n = input.count(8)?;
427 (0..n)
428 .map(|_| input.u64().map(|v| v as i64))
429 .collect::<Result<_>>()?
430 }),
431 2 => MemberColumn::Boolean(input.bytes()?),
432 3 => MemberColumn::Time(input.u32s()?),
433 4 => MemberColumn::Text(TextColumn {
434 offsets: input.u32s()?,
435 text: String::from_utf8(input.bytes()?)?,
436 }),
437 5 => {
438 let n = input.count(16)?;
439 let mut v = Vec::with_capacity(n);
440 for _ in 0..n {
441 let mut uuid = [0; 16];
442 input.read(&mut uuid)?;
443 v.push(uuid);
444 }
445 MemberColumn::Uuid(v)
446 }
447 6 => MemberColumn::Number(TextColumn {
448 offsets: input.u32s()?,
449 text: String::from_utf8(input.bytes()?)?,
450 }),
451 SORTED_IDS => {
452 let n = input.count(1)?;
453 MemberColumn::Id(super::varint::decode_u64(n, &input.bytes()?)?)
454 }
455 _ => bail!("Unknown member field type"),
456 });
457 }
458 ensure!(
459 input.remaining == 0 && refset == metadata.refset,
460 "Member manifest counts differ or trailing bytes"
461 );
462 let table = Self {
463 refset,
464 names,
465 columns,
466 };
467 ensure!(
468 table.len() == metadata.rows,
469 "Member manifest counts differ"
470 );
471 table.validate()?;
472 Ok(table)
473 }
474}
475
476pub(crate) fn valid_time(value: u32) -> bool {
477 if value == 0 {
478 return true;
479 }
480 let (year, month, day) = (value / 10000, value / 100 % 100, value % 100);
481 let days = match month {
482 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
483 4 | 6 | 9 | 11 => 30,
484 2 => 28 + u32::from(year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)),
485 _ => 0,
486 };
487 (1000..=9999).contains(&year) && day >= 1 && day <= days
488}
489
490#[derive(Debug)]
492struct Slot {
493 meta: MemberManifest,
494 fields: Vec<String>,
496 table: OnceLock<std::result::Result<MemberTable, String>>,
497 orders: Box<[OnceLock<Vec<u32>>]>,
498}
499impl Slot {
500 fn new(
501 meta: MemberManifest,
502 table: OnceLock<std::result::Result<MemberTable, String>>,
503 ) -> Self {
504 let fields = meta.fields.clone();
505 let orders = (0..fields.len()).map(|_| OnceLock::new()).collect();
506 Self {
507 meta,
508 fields,
509 table,
510 orders,
511 }
512 }
513}
514
515#[derive(Debug, Default)]
516pub struct MemberStore {
517 source: Option<IndexSource>,
518 tables: BTreeMap<u64, Slot>,
519 available: bool,
520}
521impl MemberStore {
522 pub fn loaded(tables: Vec<MemberTable>) -> Result<Self> {
523 let mut result = Self {
524 available: true,
525 ..Self::default()
526 };
527 for table in tables {
528 table.validate()?;
529 let meta = MemberManifest {
530 refset: table.refset,
531 bytes: 0,
532 sha256: String::new(),
533 rows: table.len(),
534 fields: table.names.clone(),
535 };
536 ensure!(
537 result
538 .tables
539 .insert(table.refset, Slot::new(meta, OnceLock::from(Ok(table))))
540 .is_none(),
541 "Duplicate member table"
542 );
543 }
544 Ok(result)
545 }
546 pub(super) fn lazy(source: &IndexSource, metadata: Vec<MemberManifest>) -> Result<Self> {
547 let mut store = Self {
548 source: Some(source.clone()),
549 available: true,
550 ..Self::default()
551 };
552 for meta in metadata {
553 ensure!(
554 meta.refset > 0
555 && store
556 .tables
557 .insert(meta.refset, Slot::new(meta, OnceLock::new()))
558 .is_none(),
559 "Duplicate member manifest"
560 );
561 }
562 Ok(store)
563 }
564 pub fn is_available(&self) -> bool {
565 self.available
566 }
567 pub fn refsets(&self) -> impl Iterator<Item = u64> + '_ {
568 self.tables.keys().copied()
569 }
570 pub fn fields(&self, refset: u64) -> Option<&[String]> {
571 self.tables.get(&refset).map(|slot| slot.fields.as_slice())
572 }
573 pub fn get(&self, refset: u64) -> Result<Option<&MemberTable>> {
574 let Some(slot) = self.tables.get(&refset) else {
575 return Ok(None);
576 };
577 match slot.table.get_or_init(|| {
578 MemberTable::open(self.source.as_ref().unwrap(), &slot.meta).map_err(|e| e.to_string())
579 }) {
580 Ok(table) => Ok(Some(table)),
581 Err(e) => bail!("Member index: {e}"),
582 }
583 }
584 pub fn order(&self, refset: u64, column: usize) -> Result<Option<&[u32]>> {
587 let Some(MemberColumn::Id(values)) = self.get(refset)?.and_then(|t| t.columns.get(column))
588 else {
589 return Ok(None);
590 };
591 let order = self.tables[&refset].orders[column].get_or_init(|| {
592 let mut rows: Vec<u32> = (0..values.len() as u32).collect();
593 rows.sort_by_key(|&row| values[row as usize]);
594 rows
595 });
596 Ok(Some(order))
597 }
598}