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