1use crate::error::{DxError, Result};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TypeHint {
8 Int,
10 String,
12 Float,
14 Bool,
16 Base62,
18 AutoIncrement,
20 Auto,
22}
23
24impl TypeHint {
25 pub fn from_byte(b: u8) -> Result<Self> {
27 match b {
28 b'i' => Ok(TypeHint::Int),
29 b's' => Ok(TypeHint::String),
30 b'f' => Ok(TypeHint::Float),
31 b'b' => Ok(TypeHint::Bool),
32 b'x' => Ok(TypeHint::Base62),
33 b'#' => Ok(TypeHint::AutoIncrement),
34 _ => Err(DxError::InvalidTypeHint(format!(
35 "Unknown type hint: {}",
36 b as char
37 ))),
38 }
39 }
40
41 pub fn to_byte(self) -> u8 {
43 match self {
44 TypeHint::Int => b'i',
45 TypeHint::String => b's',
46 TypeHint::Base62 => b'x',
47 TypeHint::AutoIncrement => b'#',
48 TypeHint::Float => b'f',
49 TypeHint::Bool => b'b',
50 TypeHint::Auto => b'a',
51 }
52 }
53
54 pub fn name(self) -> &'static str {
56 match self {
57 TypeHint::Int => "int",
58 TypeHint::String => "string",
59 TypeHint::Base62 => "base62",
60 TypeHint::AutoIncrement => "auto-increment",
61 TypeHint::Float => "float",
62 TypeHint::Bool => "bool",
63 TypeHint::Auto => "auto",
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct Column {
71 pub name: String,
73 pub type_hint: TypeHint,
75}
76
77impl Column {
78 pub fn new(name: String, type_hint: TypeHint) -> Self {
80 Self { name, type_hint }
81 }
82
83 pub fn is_anonymous_auto_increment(&self) -> bool {
85 self.name == "#" && self.type_hint == TypeHint::AutoIncrement
86 }
87}
88
89#[derive(Debug, Clone, PartialEq)]
91pub struct Schema {
92 pub name: String,
94 pub columns: Vec<Column>,
96}
97
98impl Schema {
99 pub fn new(name: String) -> Self {
101 Self {
102 name,
103 columns: Vec::new(),
104 }
105 }
106
107 pub fn with_columns(name: String, columns: Vec<Column>) -> Self {
109 Self { name, columns }
110 }
111
112 pub fn add_column(&mut self, name: String, type_hint: TypeHint) {
114 self.columns.push(Column::new(name, type_hint));
115 }
116
117 pub fn parse_definition(name: String, def: &str) -> Result<Self> {
119 let mut schema = Schema::new(name);
120 let parts: Vec<&str> = def.split_whitespace().collect();
121
122 let mut i = 0;
123 while i < parts.len() {
124 let part = parts[i];
125
126 if let Some(pos) = part.find('%') {
128 let col_name = &part[..pos];
129 let type_char = part.as_bytes().get(pos + 1).ok_or_else(|| {
130 DxError::SchemaError(format!("Missing type after % in '{}'", part))
131 })?;
132 let type_hint = TypeHint::from_byte(*type_char)?;
133 schema.add_column(col_name.to_string(), type_hint);
134 } else {
135 if i + 1 < parts.len() && parts[i + 1].starts_with('%') {
137 let type_str = &parts[i + 1][1..];
138 let type_hint = if type_str.is_empty() {
139 TypeHint::Auto
140 } else {
141 TypeHint::from_byte(type_str.as_bytes()[0])?
142 };
143 schema.add_column(part.to_string(), type_hint);
144 i += 1; } else {
146 schema.add_column(part.to_string(), TypeHint::Auto);
148 }
149 }
150 i += 1;
151 }
152
153 if schema.columns.is_empty() {
154 return Err(DxError::SchemaError(
155 "Schema must have at least one column".to_string(),
156 ));
157 }
158
159 Ok(schema)
160 }
161
162 pub fn column_index(&self, name: &str) -> Option<usize> {
164 self.columns.iter().position(|c| c.name == name)
165 }
166
167 pub fn column(&self, idx: usize) -> Option<&Column> {
169 self.columns.get(idx)
170 }
171
172 pub fn len(&self) -> usize {
174 self.columns.len()
175 }
176
177 pub fn is_empty(&self) -> bool {
179 self.columns.is_empty()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_parse_schema() {
189 let schema =
190 Schema::parse_definition("users".to_string(), "id%i name%s age%i active%b score%f")
191 .unwrap();
192
193 assert_eq!(schema.name, "users");
194 assert_eq!(schema.columns.len(), 5);
195 assert_eq!(schema.columns[0].name, "id");
196 assert_eq!(schema.columns[0].type_hint, TypeHint::Int);
197 assert_eq!(schema.columns[1].name, "name");
198 assert_eq!(schema.columns[1].type_hint, TypeHint::String);
199 }
200
201 #[test]
202 fn test_type_hint_roundtrip() {
203 let hints = [
204 TypeHint::Int,
205 TypeHint::String,
206 TypeHint::Float,
207 TypeHint::Bool,
208 ];
209
210 for hint in hints {
211 let byte = hint.to_byte();
212 let parsed = TypeHint::from_byte(byte).unwrap();
213 assert_eq!(hint, parsed);
214 }
215 }
216}