1use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::fmt;
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub enum SochValue {
40 Null,
41 Bool(bool),
42 Int(i64),
43 UInt(u64),
44 Float(f64),
45 Text(String),
46 Binary(Vec<u8>),
47 Array(Vec<SochValue>),
48 Object(HashMap<String, SochValue>),
49 Ref {
51 table: String,
52 id: u64,
53 },
54}
55
56impl SochValue {
57 pub fn is_null(&self) -> bool {
58 matches!(self, SochValue::Null)
59 }
60
61 pub fn as_int(&self) -> Option<i64> {
62 match self {
63 SochValue::Int(v) => Some(*v),
64 SochValue::UInt(v) => Some(*v as i64),
65 _ => None,
66 }
67 }
68
69 pub fn as_uint(&self) -> Option<u64> {
70 match self {
71 SochValue::UInt(v) => Some(*v),
72 SochValue::Int(v) if *v >= 0 => Some(*v as u64),
73 _ => None,
74 }
75 }
76
77 pub fn as_float(&self) -> Option<f64> {
78 match self {
79 SochValue::Float(v) => Some(*v),
80 SochValue::Int(v) => Some(*v as f64),
81 SochValue::UInt(v) => Some(*v as f64),
82 _ => None,
83 }
84 }
85
86 pub fn as_text(&self) -> Option<&str> {
87 match self {
88 SochValue::Text(s) => Some(s),
89 _ => None,
90 }
91 }
92
93 pub fn as_bool(&self) -> Option<bool> {
94 match self {
95 SochValue::Bool(b) => Some(*b),
96 _ => None,
97 }
98 }
99}
100
101fn needs_quoting(s: &str) -> bool {
102 if s.is_empty() { return true; }
103 if s.starts_with(' ') || s.ends_with(' ') { return true; }
104 if matches!(s, "true" | "false" | "null") { return true; }
105
106 if s.parse::<f64>().is_ok() { return true; }
108 if s == "-" || s.starts_with('-') { return true; }
109 if s.len() > 1 && s.starts_with('0') && s.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) && !s.contains('.') {
111 return true;
112 }
113
114 s.contains(|c| matches!(c, ':' | '"' | '\\' | '[' | ']' | '{' | '}' | '\n' | '\r' | '\t' | ','))
117}
118
119impl fmt::Display for SochValue {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 SochValue::Null => write!(f, "null"),
123 SochValue::Bool(b) => write!(f, "{}", b),
124 SochValue::Int(i) => write!(f, "{}", i),
125 SochValue::UInt(u) => write!(f, "{}", u),
126 SochValue::Float(fl) => write!(f, "{}", fl),
127 SochValue::Text(s) => {
128 if needs_quoting(s) {
129 write!(f, "\"")?;
130 for c in s.chars() {
131 match c {
132 '"' => write!(f, "\\\"")?,
133 '\\' => write!(f, "\\\\")?,
134 '\n' => write!(f, "\\n")?,
135 '\r' => write!(f, "\\r")?,
136 '\t' => write!(f, "\\t")?,
137 c => write!(f, "{}", c)?,
138 }
139 }
140 write!(f, "\"")
141 } else {
142 write!(f, "{}", s)
143 }
144 }
145 SochValue::Binary(b) => write!(f, "0x{}", hex::encode(b)),
146 SochValue::Array(arr) => {
147 write!(f, "[")?;
148 for (i, v) in arr.iter().enumerate() {
149 if i > 0 {
150 write!(f, ";")?;
151 }
152 write!(f, "{}", v)?;
153 }
154 write!(f, "]")
155 }
156 SochValue::Object(obj) => {
157 write!(f, "{{")?;
158 for (i, (k, v)) in obj.iter().enumerate() {
159 if i > 0 {
160 write!(f, ";")?;
161 }
162 write!(f, "{}:{}", k, v)?;
163 }
164 write!(f, "}}")
165 }
166 SochValue::Ref { table, id } => write!(f, "@{}:{}", table, id),
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub enum SochType {
174 Null,
175 Bool,
176 Int,
177 UInt,
178 Float,
179 Text,
180 Binary,
181 Array(Box<SochType>),
182 Object(Vec<(String, SochType)>),
183 Ref(String), Optional(Box<SochType>),
186}
187
188impl SochType {
189 pub fn matches(&self, value: &SochValue) -> bool {
191 match (self, value) {
192 (SochType::Null, SochValue::Null) => true,
193 (SochType::Bool, SochValue::Bool(_)) => true,
194 (SochType::Int, SochValue::Int(_)) => true,
195 (SochType::UInt, SochValue::UInt(_)) => true,
196 (SochType::Float, SochValue::Float(_)) => true,
197 (SochType::Text, SochValue::Text(_)) => true,
198 (SochType::Binary, SochValue::Binary(_)) => true,
199 (SochType::Array(inner), SochValue::Array(arr)) => arr.iter().all(|v| inner.matches(v)),
200 (SochType::Ref(table), SochValue::Ref { table: t, .. }) => table == t,
201 (SochType::Optional(inner), value) => value.is_null() || inner.matches(value),
202 _ => false,
203 }
204 }
205
206 pub fn parse(s: &str) -> Option<Self> {
208 let s = s.trim();
209 match s {
210 "null" => Some(SochType::Null),
211 "bool" => Some(SochType::Bool),
212 "int" | "i64" => Some(SochType::Int),
213 "uint" | "u64" => Some(SochType::UInt),
214 "float" | "f64" => Some(SochType::Float),
215 "text" | "string" => Some(SochType::Text),
216 "binary" | "bytes" => Some(SochType::Binary),
217 _ if s.starts_with("ref(") && s.ends_with(')') => {
218 let table = &s[4..s.len() - 1];
219 Some(SochType::Ref(table.to_string()))
220 }
221 _ if s.starts_with("array(") && s.ends_with(')') => {
222 let inner = &s[6..s.len() - 1];
223 SochType::parse(inner).map(|t| SochType::Array(Box::new(t)))
224 }
225 _ if s.ends_with('?') => {
226 let inner = &s[..s.len() - 1];
227 SochType::parse(inner).map(|t| SochType::Optional(Box::new(t)))
228 }
229 _ => None,
230 }
231 }
232}
233
234impl fmt::Display for SochType {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 match self {
237 SochType::Null => write!(f, "null"),
238 SochType::Bool => write!(f, "bool"),
239 SochType::Int => write!(f, "int"),
240 SochType::UInt => write!(f, "uint"),
241 SochType::Float => write!(f, "float"),
242 SochType::Text => write!(f, "text"),
243 SochType::Binary => write!(f, "binary"),
244 SochType::Array(inner) => write!(f, "array({})", inner),
245 SochType::Object(fields) => {
246 write!(f, "{{")?;
247 for (i, (name, ty)) in fields.iter().enumerate() {
248 if i > 0 {
249 write!(f, ",")?;
250 }
251 write!(f, "{}:{}", name, ty)?;
252 }
253 write!(f, "}}")
254 }
255 SochType::Ref(table) => write!(f, "ref({})", table),
256 SochType::Optional(inner) => write!(f, "{}?", inner),
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263pub struct SochSchema {
264 pub name: String,
266 pub fields: Vec<SochField>,
268 pub primary_key: Option<String>,
270 pub indexes: Vec<SochIndex>,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct SochField {
277 pub name: String,
278 pub field_type: SochType,
279 pub nullable: bool,
280 pub default: Option<String>, }
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285pub struct SochIndex {
286 pub name: String,
287 pub fields: Vec<String>,
288 pub unique: bool,
289}
290
291impl SochSchema {
292 pub fn new(name: impl Into<String>) -> Self {
293 Self {
294 name: name.into(),
295 fields: Vec::new(),
296 primary_key: None,
297 indexes: Vec::new(),
298 }
299 }
300
301 pub fn field(mut self, name: impl Into<String>, field_type: SochType) -> Self {
302 self.fields.push(SochField {
303 name: name.into(),
304 field_type,
305 nullable: false,
306 default: None,
307 });
308 self
309 }
310
311 pub fn nullable_field(mut self, name: impl Into<String>, field_type: SochType) -> Self {
312 self.fields.push(SochField {
313 name: name.into(),
314 field_type,
315 nullable: true,
316 default: None,
317 });
318 self
319 }
320
321 pub fn primary_key(mut self, field: impl Into<String>) -> Self {
322 self.primary_key = Some(field.into());
323 self
324 }
325
326 pub fn index(mut self, name: impl Into<String>, fields: Vec<String>, unique: bool) -> Self {
327 self.indexes.push(SochIndex {
328 name: name.into(),
329 fields,
330 unique,
331 });
332 self
333 }
334
335 pub fn field_names(&self) -> Vec<&str> {
337 self.fields.iter().map(|f| f.name.as_str()).collect()
338 }
339
340 pub fn format_header(&self) -> String {
342 let fields: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
343 format!("{}[0]{{{}}}:", self.name, fields.join(","))
344 }
345}
346
347#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
349pub struct SochRow {
350 pub values: Vec<SochValue>,
351}
352
353impl SochRow {
354 pub fn new(values: Vec<SochValue>) -> Self {
355 Self { values }
356 }
357
358 pub fn get(&self, index: usize) -> Option<&SochValue> {
360 self.values.get(index)
361 }
362
363 pub fn format(&self) -> String {
365 self.values
366 .iter()
367 .map(|v| v.to_string())
368 .collect::<Vec<_>>()
369 .join(",")
370 }
371
372 pub fn parse(line: &str, schema: &SochSchema) -> Result<Self, String> {
374 let mut values = Vec::with_capacity(schema.fields.len());
375 let mut chars = line.chars().peekable();
376 let mut current = String::new();
377 let mut in_quotes = false;
378 let mut field_idx = 0;
379
380 while let Some(ch) = chars.next() {
381 match ch {
382 '"' if !in_quotes => {
383 in_quotes = true;
384 }
385 '"' if in_quotes => {
386 if chars.peek() == Some(&'"') {
387 chars.next();
388 current.push('"');
389 } else {
390 in_quotes = false;
391 }
392 }
393 ',' if !in_quotes => {
394 let value = Self::parse_value(¤t, field_idx, schema)?;
395 values.push(value);
396 current.clear();
397 field_idx += 1;
398 }
399 _ => {
400 current.push(ch);
401 }
402 }
403 }
404
405 if !current.is_empty() || field_idx < schema.fields.len() {
407 let value = Self::parse_value(¤t, field_idx, schema)?;
408 values.push(value);
409 }
410
411 Ok(Self { values })
412 }
413
414 fn parse_value(s: &str, field_idx: usize, schema: &SochSchema) -> Result<SochValue, String> {
415 let s = s.trim();
416
417 if s.is_empty() || s == "null" {
418 return Ok(SochValue::Null);
419 }
420
421 let field = schema
422 .fields
423 .get(field_idx)
424 .ok_or_else(|| format!("Field index {} out of bounds", field_idx))?;
425
426 match &field.field_type {
427 SochType::Bool => match s.to_lowercase().as_str() {
428 "true" | "1" | "yes" => Ok(SochValue::Bool(true)),
429 "false" | "0" | "no" => Ok(SochValue::Bool(false)),
430 _ => Err(format!("Invalid bool: {}", s)),
431 },
432 SochType::Int => s
433 .parse::<i64>()
434 .map(SochValue::Int)
435 .map_err(|e| format!("Invalid int: {}", e)),
436 SochType::UInt => s
437 .parse::<u64>()
438 .map(SochValue::UInt)
439 .map_err(|e| format!("Invalid uint: {}", e)),
440 SochType::Float => s
441 .parse::<f64>()
442 .map(SochValue::Float)
443 .map_err(|e| format!("Invalid float: {}", e)),
444 SochType::Text => Ok(SochValue::Text(s.to_string())),
445 SochType::Binary => {
446 if let Some(hex_str) = s.strip_prefix("0x") {
447 hex::decode(hex_str)
448 .map(SochValue::Binary)
449 .map_err(|e| format!("Invalid hex: {}", e))
450 } else {
451 Err("Binary must start with 0x".to_string())
452 }
453 }
454 SochType::Ref(table) => {
455 if let Some(ref_str) = s.strip_prefix('@') {
457 let parts: Vec<&str> = ref_str.split(':').collect();
458 if parts.len() == 2 {
459 let id = parts[1]
460 .parse::<u64>()
461 .map_err(|e| format!("Invalid ref id: {}", e))?;
462 Ok(SochValue::Ref {
463 table: parts[0].to_string(),
464 id,
465 })
466 } else {
467 Err(format!("Invalid ref format: {}", s))
468 }
469 } else {
470 let id = s
471 .parse::<u64>()
472 .map_err(|e| format!("Invalid ref id: {}", e))?;
473 Ok(SochValue::Ref {
474 table: table.clone(),
475 id,
476 })
477 }
478 }
479 SochType::Optional(inner) => {
480 let temp_field = SochField {
482 name: field.name.clone(),
483 field_type: (**inner).clone(),
484 nullable: true,
485 default: None,
486 };
487 let temp_schema = SochSchema {
488 name: schema.name.clone(),
489 fields: vec![temp_field],
490 primary_key: None,
491 indexes: vec![],
492 };
493 Self::parse_value(s, 0, &temp_schema)
494 }
495 _ => Ok(SochValue::Text(s.to_string())),
496 }
497 }
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct SochTable {
503 pub schema: SochSchema,
504 pub rows: Vec<SochRow>,
505}
506
507impl SochTable {
508 pub fn new(schema: SochSchema) -> Self {
509 Self {
510 schema,
511 rows: Vec::new(),
512 }
513 }
514
515 pub fn with_rows(schema: SochSchema, rows: Vec<SochRow>) -> Self {
516 Self { schema, rows }
517 }
518
519 pub fn push(&mut self, row: SochRow) {
520 self.rows.push(row);
521 }
522
523 pub fn len(&self) -> usize {
524 self.rows.len()
525 }
526
527 pub fn is_empty(&self) -> bool {
528 self.rows.is_empty()
529 }
530
531 pub fn format(&self) -> String {
533 let fields: Vec<&str> = self.schema.fields.iter().map(|f| f.name.as_str()).collect();
534 let header = format!(
535 "{}[{}]{{{}}}:",
536 self.schema.name,
537 self.rows.len(),
538 fields.join(",")
539 );
540
541 let mut output = header;
542 for row in &self.rows {
543 output.push('\n');
544 output.push_str(&row.format());
545 }
546 output
547 }
548
549 pub fn parse(input: &str) -> Result<Self, String> {
551 let mut lines = input.lines();
552
553 let header = lines.next().ok_or("Empty input")?;
555 let (schema, _count) = Self::parse_header(header)?;
556
557 let mut rows = Vec::new();
559 for line in lines {
560 if line.trim().is_empty() {
561 continue;
562 }
563 let row = SochRow::parse(line, &schema)?;
564 rows.push(row);
565 }
566
567 Ok(Self { schema, rows })
568 }
569
570 fn parse_header(header: &str) -> Result<(SochSchema, usize), String> {
571 let header = header.trim_end_matches(':');
573
574 let bracket_start = header.find('[').ok_or("Missing [")?;
575 let bracket_end = header.find(']').ok_or("Missing ]")?;
576 let brace_start = header.find('{').ok_or("Missing {")?;
577 let brace_end = header.find('}').ok_or("Missing }")?;
578
579 let name = &header[..bracket_start];
580 let count_str = &header[bracket_start + 1..bracket_end];
581 let fields_str = &header[brace_start + 1..brace_end];
582
583 let count = count_str
584 .parse::<usize>()
585 .map_err(|e| format!("Invalid count: {}", e))?;
586
587 let field_names: Vec<&str> = fields_str.split(',').map(|s| s.trim()).collect();
588
589 let mut schema = SochSchema::new(name);
590 for field_name in field_names {
591 if let Some(colon_pos) = field_name.find(':') {
593 let fname = &field_name[..colon_pos];
594 let ftype_str = &field_name[colon_pos + 1..];
595 let ftype = SochType::parse(ftype_str).unwrap_or(SochType::Text);
596 schema.fields.push(SochField {
597 name: fname.to_string(),
598 field_type: ftype,
599 nullable: false,
600 default: None,
601 });
602 } else {
603 schema.fields.push(SochField {
605 name: field_name.to_string(),
606 field_type: SochType::Text,
607 nullable: false,
608 default: None,
609 });
610 }
611 }
612
613 Ok((schema, count))
614 }
615}
616
617impl fmt::Display for SochTable {
618 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619 write!(f, "{}", self.format())
620 }
621}
622
623pub trait ColumnAccess {
625 fn row_count(&self) -> usize;
626 fn col_count(&self) -> usize;
627 fn field_names(&self) -> Vec<&str>;
628 fn write_value(
629 &self,
630 col_idx: usize,
631 row_idx: usize,
632 f: &mut dyn std::fmt::Write,
633 ) -> std::fmt::Result;
634}
635
636pub struct SochCursor<'a, C: ColumnAccess> {
638 access: &'a C,
639 current_row: usize,
640 header_emitted: bool,
641 schema_name: String,
642}
643
644impl<'a, C: ColumnAccess> SochCursor<'a, C> {
645 pub fn new(access: &'a C, schema_name: String) -> Self {
646 Self {
647 access,
648 current_row: 0,
649 header_emitted: false,
650 schema_name,
651 }
652 }
653}
654
655impl<'a, C: ColumnAccess> Iterator for SochCursor<'a, C> {
656 type Item = String;
657
658 fn next(&mut self) -> Option<Self::Item> {
659 if !self.header_emitted {
660 self.header_emitted = true;
661 let fields = self.access.field_names().join(",");
662 return Some(format!(
663 "{}[{}]{{{}}}:",
664 self.schema_name,
665 self.access.row_count(),
666 fields
667 ));
668 }
669
670 if self.current_row >= self.access.row_count() {
671 return None;
672 }
673
674 let mut row_str = String::new();
675 for col_idx in 0..self.access.col_count() {
676 if col_idx > 0 {
677 row_str.push(',');
678 }
679 let _ = self
681 .access
682 .write_value(col_idx, self.current_row, &mut row_str);
683 }
684
685 self.current_row += 1;
686 Some(row_str)
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn test_soch_value_display() {
696 assert_eq!(SochValue::Int(42).to_string(), "42");
697 assert_eq!(SochValue::Text("hello".into()).to_string(), "hello");
698 assert_eq!(
699 SochValue::Text("hello, world".into()).to_string(),
700 "\"hello, world\""
701 );
702 assert_eq!(SochValue::Bool(true).to_string(), "true");
703 assert_eq!(SochValue::Null.to_string(), "null");
704 }
705
706 #[test]
707 fn test_soch_schema() {
708 let schema = SochSchema::new("users")
709 .field("id", SochType::UInt)
710 .field("name", SochType::Text)
711 .field("email", SochType::Text)
712 .primary_key("id");
713
714 assert_eq!(schema.name, "users");
715 assert_eq!(schema.fields.len(), 3);
716 assert_eq!(schema.primary_key, Some("id".to_string()));
717 }
718
719 #[test]
720 fn test_soch_table_format() {
721 let schema = SochSchema::new("users")
722 .field("id", SochType::UInt)
723 .field("name", SochType::Text)
724 .field("email", SochType::Text);
725
726 let mut table = SochTable::new(schema);
727 table.push(SochRow::new(vec![
728 SochValue::UInt(1),
729 SochValue::Text("Alice".into()),
730 SochValue::Text("alice@example.com".into()),
731 ]));
732 table.push(SochRow::new(vec![
733 SochValue::UInt(2),
734 SochValue::Text("Bob".into()),
735 SochValue::Text("bob@example.com".into()),
736 ]));
737
738 let formatted = table.format();
739 assert!(formatted.contains("users[2]{id,name,email}:"));
740 assert!(formatted.contains("1,Alice,alice@example.com"));
741 assert!(formatted.contains("2,Bob,bob@example.com"));
742 }
743
744 #[test]
745 fn test_soch_table_parse() {
746 let input = r#"users[2]{id,name,email}:
7471,Alice,alice@example.com
7482,Bob,bob@example.com"#;
749
750 let table = SochTable::parse(input).unwrap();
751 assert_eq!(table.schema.name, "users");
752 assert_eq!(table.rows.len(), 2);
753 }
754
755 #[test]
756 fn test_soch_type_parse() {
757 assert_eq!(SochType::parse("int"), Some(SochType::Int));
758 assert_eq!(SochType::parse("text"), Some(SochType::Text));
759 assert_eq!(
760 SochType::parse("ref(users)"),
761 Some(SochType::Ref("users".into()))
762 );
763 assert_eq!(
764 SochType::parse("int?"),
765 Some(SochType::Optional(Box::new(SochType::Int)))
766 );
767 }
768}