1use jsonschema::Validator;
2use lazy_static::lazy_static;
3use log::*;
4use regex::Regex;
5use std::borrow::Borrow;
6use std::collections::HashMap;
7use std::hash::{Hash, Hasher};
8use std::sync::Arc;
9use thiserror::Error;
10
11use crate::sqlite::{Column, ColumnDataType, ColumnOption, QualifiedName, Table, View};
12
13#[derive(Debug, Clone, Error)]
15pub enum JsonSchemaError {
16 #[error("Schema compile error: {0}")]
17 SchemaCompile(String),
18 #[error("Validation error")]
19 Validation,
20 #[error("Schema not found: {0}")]
21 NotFound(String),
22 #[error("Json serialization error: {0}")]
23 JsonSerialization(Arc<serde_json::Error>),
24}
25
26#[derive(Clone, Debug, PartialEq)]
27pub enum JsonColumnMetadata {
28 SchemaName(String),
29 Pattern(serde_json::Value),
30}
31
32impl JsonColumnMetadata {
33 pub fn validate(&self, value: &serde_json::Value) -> Result<(), JsonSchemaError> {
34 match self {
35 Self::SchemaName(name) => {
36 let Some(schema) = crate::registry::get_compiled_schema(name) else {
37 return Err(JsonSchemaError::NotFound(name.to_string()));
38 };
39 schema
40 .validate(value)
41 .map_err(|_err| JsonSchemaError::Validation)?;
42 return Ok(());
43 }
44 Self::Pattern(pattern) => {
45 let schema =
46 Validator::new(pattern).map_err(|err| JsonSchemaError::SchemaCompile(err.to_string()))?;
47 if !schema.is_valid(value) {
48 Err(JsonSchemaError::Validation)
49 } else {
50 Ok(())
51 }
52 }
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct JsonMetadata {
59 pub columns: Vec<Option<JsonColumnMetadata>>,
60
61 file_column_indexes: Vec<usize>,
63}
64
65impl JsonMetadata {
66 pub fn has_file_columns(&self) -> bool {
67 return !self.file_column_indexes.is_empty();
68 }
69
70 pub fn file_column_indexes(&self) -> &[usize] {
72 return &self.file_column_indexes;
73 }
74
75 fn from_table(table: &Table) -> Self {
76 return Self::from_columns(&table.columns);
77 }
78
79 fn from_view(view: &View) -> Option<Self> {
80 return view.columns.as_ref().map(|cols| Self::from_columns(cols));
81 }
82
83 fn from_columns(columns: &[Column]) -> Self {
84 let columns: Vec<_> = columns.iter().map(build_json_metadata).collect();
85
86 let file_column_indexes = find_file_column_indexes(&columns);
87
88 return Self {
89 columns,
90 file_column_indexes,
91 };
92 }
93}
94
95#[derive(Debug, Clone)]
99pub struct TableMetadata {
100 pub schema: Table,
101
102 pub record_pk_column: Option<usize>,
104 pub user_id_columns: Vec<usize>,
106 pub json_metadata: JsonMetadata,
108
109 name_to_index: HashMap<String, usize>,
110 }
112
113impl TableMetadata {
114 pub fn new(table: Table, tables: &[Table], user_table_name: &str) -> Self {
119 let name_to_index = HashMap::<String, usize>::from_iter(
120 table
121 .columns
122 .iter()
123 .enumerate()
124 .map(|(index, col)| (col.name.clone(), index)),
125 );
126
127 let record_pk_column = find_record_pk_column_index(&table.columns, tables);
128 let user_id_columns = find_user_id_foreign_key_columns(&table.columns, user_table_name);
129 let json_metadata = JsonMetadata::from_table(&table);
130
131 return TableMetadata {
132 schema: table,
133 name_to_index,
134 record_pk_column,
135 user_id_columns,
136 json_metadata,
137 };
138 }
139
140 #[inline]
141 pub fn name(&self) -> &QualifiedName {
142 return &self.schema.name;
143 }
144
145 #[inline]
146 pub fn column_index_by_name(&self, key: &str) -> Option<usize> {
147 return self.name_to_index.get(key).copied();
148 }
149
150 #[inline]
151 pub fn column_by_name(&self, key: &str) -> Option<(usize, &Column)> {
152 let index = self.column_index_by_name(key)?;
153 return Some((index, &self.schema.columns[index]));
154 }
155}
156
157impl PartialEq for TableMetadata {
160 fn eq(&self, other: &Self) -> bool {
161 return self.schema.name == other.schema.name;
162 }
163}
164
165impl Eq for TableMetadata {}
166
167impl Hash for TableMetadata {
170 fn hash<H: Hasher>(&self, state: &mut H) {
171 self.schema.name.hash(state);
172 }
173}
174
175impl Borrow<QualifiedName> for TableMetadata {
178 fn borrow(&self) -> &QualifiedName {
179 return &self.schema.name;
180 }
181}
182
183impl Borrow<QualifiedName> for Arc<TableMetadata> {
186 fn borrow(&self) -> &QualifiedName {
187 return &self.schema.name;
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct ViewMetadata {
194 pub schema: View,
195
196 name_to_index: HashMap<String, usize>,
197 record_pk_column: Option<usize>,
198 json_metadata: Option<JsonMetadata>,
199}
200
201impl ViewMetadata {
202 pub fn new(view: View, tables: &[Table]) -> Self {
207 let name_to_index = if let Some(ref columns) = view.columns {
208 HashMap::<String, usize>::from_iter(
209 columns
210 .iter()
211 .enumerate()
212 .map(|(index, col)| (col.name.clone(), index)),
213 )
214 } else {
215 HashMap::<String, usize>::new()
216 };
217
218 let record_pk_column = view
219 .columns
220 .as_ref()
221 .and_then(|c| find_record_pk_column_index(c, tables));
222 let json_metadata = JsonMetadata::from_view(&view);
223
224 return ViewMetadata {
225 schema: view,
226 name_to_index,
227 record_pk_column,
228 json_metadata,
229 };
230 }
231
232 #[inline]
233 pub fn name(&self) -> &QualifiedName {
234 &self.schema.name
235 }
236
237 #[inline]
238 pub fn column_index_by_name(&self, key: &str) -> Option<usize> {
239 self.name_to_index.get(key).copied()
240 }
241
242 #[inline]
243 pub fn column_by_name(&self, key: &str) -> Option<(usize, &Column)> {
244 let index = self.column_index_by_name(key)?;
245 let cols = self.schema.columns.as_ref()?;
246 return Some((index, &cols[index]));
247 }
248}
249
250impl PartialEq for ViewMetadata {
253 fn eq(&self, other: &Self) -> bool {
254 return self.schema.name == other.schema.name;
255 }
256}
257
258impl Eq for ViewMetadata {}
259
260impl Hash for ViewMetadata {
263 fn hash<H: Hasher>(&self, state: &mut H) {
264 self.schema.name.hash(state);
265 }
266}
267
268impl Borrow<QualifiedName> for ViewMetadata {
271 fn borrow(&self) -> &QualifiedName {
272 return &self.schema.name;
273 }
274}
275
276impl Borrow<QualifiedName> for Arc<ViewMetadata> {
279 fn borrow(&self) -> &QualifiedName {
280 return &self.schema.name;
281 }
282}
283
284pub trait TableOrViewMetadata {
285 fn qualified_name(&self) -> &QualifiedName;
286 fn record_pk_column(&self) -> Option<(usize, &Column)>;
287 fn json_metadata(&self) -> Option<&JsonMetadata>;
288 fn columns(&self) -> Option<&[Column]>;
289}
290
291impl TableOrViewMetadata for TableMetadata {
292 fn qualified_name(&self) -> &QualifiedName {
293 return self.name();
294 }
295
296 fn columns(&self) -> Option<&[Column]> {
297 return Some(&self.schema.columns);
298 }
299
300 fn json_metadata(&self) -> Option<&JsonMetadata> {
301 return Some(&self.json_metadata);
302 }
303
304 fn record_pk_column(&self) -> Option<(usize, &Column)> {
305 let index = self.record_pk_column?;
306 return self.schema.columns.get(index).map(|c| (index, c));
307 }
308}
309
310impl TableOrViewMetadata for ViewMetadata {
311 fn qualified_name(&self) -> &QualifiedName {
312 return self.name();
313 }
314
315 fn columns(&self) -> Option<&[Column]> {
316 return self.schema.columns.as_deref();
317 }
318
319 fn json_metadata(&self) -> Option<&JsonMetadata> {
320 return self.json_metadata.as_ref();
321 }
322
323 fn record_pk_column(&self) -> Option<(usize, &Column)> {
324 let Some(columns) = &self.schema.columns else {
325 return None;
326 };
327 let index = self.record_pk_column?;
328 return columns.get(index).map(|c| (index, c));
329 }
330}
331
332fn build_json_metadata(col: &Column) -> Option<JsonColumnMetadata> {
333 for opt in &col.options {
334 match extract_json_metadata(opt) {
335 Ok(maybe) => {
336 if let Some(jm) = maybe {
337 return Some(jm);
338 }
339 }
340 Err(err) => {
341 error!("Failed to get JSON schema: {err}");
342 }
343 }
344 }
345 None
346}
347
348pub fn extract_json_metadata(
349 opt: &ColumnOption,
350) -> Result<Option<JsonColumnMetadata>, JsonSchemaError> {
351 let ColumnOption::Check(check) = opt else {
352 return Ok(None);
353 };
354
355 lazy_static! {
356 static ref SCHEMA_RE: Regex =
357 Regex::new(r#"(?smR)jsonschema\s*\(\s*[\['"](?<name>.*)[\]'"]\s*,.+?\)"#)
358 .expect("infallible");
359 static ref MATCHES_RE: Regex =
360 Regex::new(r"(?smR)jsonschema_matches\s*\(.+?(?<pattern>\{.*\}).+?\)").expect("infallible");
361 }
362
363 if let Some(cap) = SCHEMA_RE.captures(check) {
364 let name = &cap["name"];
365 let Some(_schema) = crate::registry::get_schema(name) else {
366 let schemas: Vec<String> = crate::registry::get_schemas()
367 .iter()
368 .map(|s| s.name.clone())
369 .collect();
370 return Err(JsonSchemaError::NotFound(format!(
371 "Json schema {name} not found in: {schemas:?}"
372 )));
373 };
374
375 return Ok(Some(JsonColumnMetadata::SchemaName(name.to_string())));
376 }
377
378 if let Some(cap) = MATCHES_RE.captures(check) {
379 let pattern = &cap["pattern"];
380 let value = serde_json::from_str::<serde_json::Value>(pattern)
381 .map_err(|err| JsonSchemaError::JsonSerialization(Arc::new(err)))?;
382 return Ok(Some(JsonColumnMetadata::Pattern(value)));
383 }
384
385 return Ok(None);
386}
387
388pub fn find_file_column_indexes(json_column_metadata: &[Option<JsonColumnMetadata>]) -> Vec<usize> {
389 let mut indexes: Vec<usize> = vec![];
390
391 for (index, column) in json_column_metadata.iter().enumerate() {
392 if let Some(metadata) = column {
393 match metadata {
394 JsonColumnMetadata::SchemaName(name) if name == "std.FileUpload" => {
395 indexes.push(index);
396 }
397 JsonColumnMetadata::SchemaName(name) if name == "std.FileUploads" => {
398 indexes.push(index);
399 }
400 _ => {}
401 };
402 }
403 }
404
405 return indexes;
406}
407
408pub fn find_user_id_foreign_key_columns(columns: &[Column], user_table_name: &str) -> Vec<usize> {
409 let mut indexes: Vec<usize> = vec![];
410 for (index, col) in columns.iter().enumerate() {
411 for opt in &col.options {
412 if let ColumnOption::ForeignKey {
413 foreign_table,
414 referred_columns,
415 ..
416 } = opt
417 {
418 if foreign_table == user_table_name
419 && referred_columns.len() == 1
420 && referred_columns[0] == "id"
421 {
422 indexes.push(index);
423 }
424 }
425 }
426 }
427 return indexes;
428}
429
430pub(crate) fn find_pk_column_index(columns: &[Column]) -> Option<usize> {
431 return columns.iter().position(|col| {
432 for opt in &col.options {
433 if let ColumnOption::Unique { is_primary, .. } = opt {
434 return *is_primary;
435 }
436 }
437 return false;
438 });
439}
440
441fn find_record_pk_column_index(columns: &[Column], tables: &[Table]) -> Option<usize> {
445 let index = find_pk_column_index(columns)?;
446 let column = &columns[index];
447
448 if column.data_type == ColumnDataType::Integer {
449 return Some(index);
452 }
453
454 for opts in &column.options {
455 lazy_static! {
456 static ref UUID_CHECK_RE: Regex = Regex::new(r"^is_uuid(|_v7|_v4)\s*\(").expect("infallible");
457 }
458
459 match &opts {
460 ColumnOption::ForeignKey {
462 foreign_table,
463 referred_columns,
464 ..
465 } => {
466 let Some(referred_table) = tables.iter().find(|t| t.name.name == *foreign_table) else {
469 error!("Failed to get foreign key schema for {foreign_table}");
470 continue;
471 };
472
473 if referred_columns.len() != 1 {
474 return None;
475 }
476 let referred_column = &referred_columns[0];
477
478 let col = referred_table
479 .columns
480 .iter()
481 .find(|c| c.name == *referred_column)?;
482
483 let mut is_pk = false;
484 for opt in &col.options {
485 match opt {
486 ColumnOption::Check(expr) if UUID_CHECK_RE.is_match(expr) => {
487 return Some(index);
488 }
489 ColumnOption::Unique { is_primary, .. } if *is_primary => {
490 is_pk = true;
491 }
492 _ => {}
493 }
494 }
495
496 if is_pk && col.data_type == ColumnDataType::Integer {
497 return Some(index);
498 }
499
500 return None;
501 }
502 ColumnOption::Check(expr) if UUID_CHECK_RE.is_match(expr) => {
503 return Some(index);
504 }
505 _ => {}
506 }
507 }
508
509 return None;
510}
511
512#[cfg(test)]
513mod tests {
514 use std::collections::HashSet;
515
516 use super::*;
517 use crate::sqlite::{Table, sqlite3_parse_into_statement};
518
519 #[test]
520 fn test_parse_create_view() {
521 let table_name = QualifiedName {
522 name: "table_name".to_string(),
523 database_schema: Some("main".to_string()),
524 };
525 let table_sql = format!(
526 r#"
527 CREATE TABLE {table_name} (
528 id BLOB PRIMARY KEY NOT NULL CHECK(is_uuid_v7(id)) DEFAULT (uuid_v7()),
529 col0 TEXT NOT NULL DEFAULT '',
530 col1 BLOB NOT NULL,
531 hidden INTEGER DEFAULT 42
532 ) STRICT;"#,
533 table_name = table_name.escaped_string(),
534 );
535
536 let create_table_statement = sqlite3_parse_into_statement(&table_sql).unwrap().unwrap();
537
538 let table: Table = create_table_statement.try_into().unwrap();
539
540 {
541 let metadata = TableMetadata::new(table.clone(), &[table.clone()], "_user");
542
543 assert_eq!(table_name, *metadata.name());
544 assert_eq!("col1", metadata.columns().unwrap()[2].name);
545 assert_eq!(1, *metadata.name_to_index.get("col0").unwrap());
546 }
547
548 let view_name = QualifiedName {
549 name: "view_name".to_string(),
550 database_schema: Some("main".to_string()),
551 };
552
553 {
554 let query = format!("SELECT col0, col1 FROM {}", table_name.escaped_string());
555 let view_sql = format!(
556 "CREATE VIEW {view_name} AS {query}",
557 view_name = view_name.escaped_string()
558 );
559 let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap();
560
561 let table_view = View::from(create_view_statement, &[table.clone()]).unwrap();
562
563 assert_eq!(table_view.name, view_name);
564 assert_eq!(table_view.query, query);
565 assert_eq!(table_view.temporary, false);
566
567 let view_columns = table_view.columns.as_ref().unwrap();
568
569 assert_eq!(view_columns.len(), 2);
570 assert_eq!(view_columns[0].name, "col0");
571 assert_eq!(view_columns[0].data_type, ColumnDataType::Text);
572
573 assert_eq!(view_columns[1].name, "col1");
574 assert_eq!(view_columns[1].data_type, ColumnDataType::Blob);
575
576 let view_metadata = ViewMetadata::new(table_view, &[table.clone()]);
577
578 assert!(view_metadata.record_pk_column().is_none());
579 assert_eq!(view_metadata.columns().as_ref().unwrap().len(), 2);
580 }
581
582 {
583 let query = format!("SELECT id, col0, col1 FROM {}", table_name.escaped_string());
584 let view_sql = format!(
585 "CREATE VIEW {view_name} AS {query}",
586 view_name = view_name.escaped_string()
587 );
588 let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap();
589
590 let table_view = View::from(create_view_statement, &[table.clone()]).unwrap();
591
592 assert_eq!(table_view.name, view_name);
593 assert_eq!(table_view.query, query);
594 assert_eq!(table_view.temporary, false);
595
596 let view_metadata = ViewMetadata::new(table_view, &[table.clone()]);
597
598 let uuidv7_col = view_metadata.record_pk_column().unwrap();
599 let columns = view_metadata.columns().unwrap();
600 assert_eq!(columns.len(), 3);
601 assert_eq!(columns[uuidv7_col.0].name, "id");
602 }
603 }
604
605 #[test]
606 fn test_metadata_hash_set_by_name() {
607 let table_name = QualifiedName {
608 name: "table_name".to_string(),
609 database_schema: Some("main".to_string()),
610 };
611 let table_sql = format!(
612 "CREATE TABLE {table_name} (id INTEGER PRIMARY KEY) STRICT",
613 table_name = table_name.escaped_string()
614 );
615 let create_table_statement = sqlite3_parse_into_statement(&table_sql).unwrap().unwrap();
616 let table: Table = create_table_statement.try_into().unwrap();
617 let table_metadata = TableMetadata::new(table.clone(), &[table.clone()], "_user");
618
619 let mut table_set = HashSet::<TableMetadata>::new();
620
621 assert!(table_set.insert(table_metadata.clone()));
622 assert!(table_set.get(&table_name).is_some());
623 assert_eq!(
624 table_set.get(&QualifiedName::parse("table_name").unwrap()),
625 Some(&table_metadata)
626 );
627
628 let view_name = QualifiedName {
630 name: "view_name".to_string(),
631 database_schema: Some("main".to_string()),
632 };
633 let view_sql = format!(
634 "CREATE VIEW {view_name} AS SELECT id FROM {table_name}",
635 view_name = view_name.escaped_string(),
636 table_name = table_name.escaped_string()
637 );
638 let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap();
639 let table_view = View::from(create_view_statement, &[table.clone()]).unwrap();
640 let view_metadata = Arc::new(ViewMetadata::new(table_view, &[table.clone()]));
641
642 let mut view_set = HashSet::<Arc<ViewMetadata>>::new();
643
644 assert!(view_set.insert(view_metadata.clone()));
645 assert_eq!(view_set.get(&view_name), Some(&view_metadata));
646 assert_eq!(
647 view_set.get(&QualifiedName::parse("view_name").unwrap()),
648 Some(&view_metadata)
649 );
650 }
651}