use cratefield_core::ConfigError;
use serde_json::Value;
pub const MAX_IDENTIFIER_CHARS: usize = 63;
pub const RESERVED_PREFIXES: &[&str] = &["harness_", "sqlite_", "pg_", "cf_"];
pub const RESERVED_WORDS: &[&str] = &[
"abort",
"action",
"add",
"after",
"all",
"alter",
"always",
"analyse",
"analyze",
"and",
"any",
"array",
"as",
"asc",
"asymmetric",
"attach",
"authorization",
"autoincrement",
"before",
"begin",
"between",
"binary",
"both",
"by",
"cascade",
"case",
"cast",
"check",
"cmax",
"cmin",
"collate",
"collation",
"column",
"commit",
"concurrently",
"conflict",
"constraint",
"create",
"cross",
"ctid",
"current",
"current_catalog",
"current_date",
"current_role",
"current_schema",
"current_time",
"current_timestamp",
"current_user",
"database",
"default",
"deferrable",
"deferred",
"delete",
"desc",
"detach",
"distinct",
"do",
"drop",
"each",
"else",
"end",
"escape",
"except",
"exclude",
"exclusive",
"exists",
"explain",
"fail",
"false",
"fetch",
"filter",
"first",
"following",
"for",
"foreign",
"freeze",
"from",
"full",
"generated",
"glob",
"grant",
"group",
"groups",
"having",
"if",
"ignore",
"ilike",
"immediate",
"in",
"index",
"indexed",
"initially",
"inner",
"insert",
"instead",
"intersect",
"into",
"is",
"isnull",
"join",
"key",
"last",
"lateral",
"leading",
"left",
"like",
"limit",
"localtime",
"localtimestamp",
"match",
"materialized",
"natural",
"no",
"not",
"nothing",
"notnull",
"null",
"nulls",
"of",
"offset",
"on",
"only",
"or",
"order",
"others",
"outer",
"over",
"overlaps",
"partition",
"placing",
"plan",
"pragma",
"preceding",
"primary",
"query",
"raise",
"range",
"recursive",
"references",
"regexp",
"reindex",
"release",
"rename",
"replace",
"restrict",
"returning",
"right",
"rollback",
"row",
"rows",
"savepoint",
"select",
"session_user",
"set",
"similar",
"some",
"symmetric",
"system_user",
"table",
"tableoid",
"tablesample",
"temp",
"temporary",
"then",
"ties",
"to",
"trailing",
"transaction",
"trigger",
"true",
"unbounded",
"union",
"unique",
"update",
"user",
"using",
"vacuum",
"values",
"variadic",
"verbose",
"view",
"virtual",
"when",
"where",
"window",
"with",
"without",
"xmax",
"xmin",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextFormat {
Email,
Url,
}
impl TextFormat {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
TextFormat::Email => "email",
TextFormat::Url => "url",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldKind {
Text {
min_len: Option<u32>,
max_len: Option<u32>,
format: Option<TextFormat>,
},
Integer { min: Option<i64>, max: Option<i64> },
Real { min: Option<f64>, max: Option<f64> },
Boolean,
Timestamp,
Uuid,
Json,
Enum { values: Vec<String> },
}
impl FieldKind {
#[must_use]
pub fn can_hold_a_subject(&self) -> bool {
matches!(self, FieldKind::Text { .. } | FieldKind::Uuid)
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
FieldKind::Text { .. } => "text",
FieldKind::Integer { .. } => "integer",
FieldKind::Real { .. } => "real",
FieldKind::Boolean => "boolean",
FieldKind::Timestamp => "timestamp",
FieldKind::Uuid => "uuid",
FieldKind::Json => "json",
FieldKind::Enum { .. } => "enum",
}
}
#[must_use]
pub fn text() -> Self {
FieldKind::Text {
min_len: None,
max_len: None,
format: None,
}
}
#[must_use]
pub fn integer() -> Self {
FieldKind::Integer {
min: None,
max: None,
}
}
#[must_use]
pub fn real() -> Self {
FieldKind::Real {
min: None,
max: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FieldDef {
pub name: String,
pub kind: FieldKind,
pub required: bool,
pub unique: bool,
pub indexed: bool,
pub default: Option<Value>,
}
impl FieldDef {
#[must_use]
pub fn new(name: impl Into<String>, kind: FieldKind) -> Self {
Self {
name: name.into(),
kind,
required: false,
unique: false,
indexed: false,
default: None,
}
}
#[must_use]
pub fn required(mut self) -> Self {
self.required = true;
self
}
#[must_use]
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
#[must_use]
pub fn indexed(mut self) -> Self {
self.indexed = true;
self
}
#[must_use]
pub fn default_value(mut self, value: Value) -> Self {
self.default = Some(value);
self
}
#[must_use]
pub fn must_be_present(&self) -> bool {
self.required && self.default.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForeignKey {
pub field: String,
pub references: String,
}
impl ForeignKey {
#[must_use]
pub fn new(field: impl Into<String>, references: impl Into<String>) -> Self {
Self {
field: field.into(),
references: references.into(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TableDef {
pub name: String,
pub fields: Vec<FieldDef>,
pub primary_key: Vec<String>,
pub foreign_keys: Vec<ForeignKey>,
}
impl TableDef {
#[must_use]
pub fn new(
name: impl Into<String>,
primary_key: impl Into<String>,
fields: Vec<FieldDef>,
) -> Self {
Self {
name: name.into(),
fields,
primary_key: vec![primary_key.into()],
foreign_keys: Vec::new(),
}
}
#[must_use]
pub fn foreign_key(mut self, key: ForeignKey) -> Self {
self.foreign_keys.push(key);
self
}
#[must_use]
pub fn field(&self, name: &str) -> Option<&FieldDef> {
self.fields.iter().find(|field| field.name == name)
}
#[must_use]
pub fn is_primary_key(&self, name: &str) -> bool {
self.primary_key.iter().any(|key| key == name)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Schema {
pub tables: Vec<TableDef>,
}
impl Schema {
#[must_use]
pub fn new(tables: Vec<TableDef>) -> Self {
let mut tables = tables;
tables.sort_by(|a, b| a.name.cmp(&b.name));
Self { tables }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tables.is_empty()
}
#[must_use]
pub fn table(&self, name: &str) -> Option<&TableDef> {
self.tables.iter().find(|table| table.name == name)
}
pub fn validate(&self) -> Result<(), ConfigError> {
let mut errors = ConfigError::new();
let mut seen_tables: Vec<String> = Vec::new();
for table in &self.tables {
check_identifier(&mut errors, "table", &table.name, &table.name);
if seen_tables.iter().any(|seen| seen == &table.name) {
errors.push(format!("table `{}`: declared twice", table.name));
}
seen_tables.push(table.name.clone());
self.validate_table(&mut errors, table);
}
self.validate_index_names(&mut errors);
self.validate_no_reference_cycle(&mut errors);
errors.into_result()
}
fn validate_index_names(&self, errors: &mut ConfigError) {
let mut generated: Vec<(String, String, String)> = Vec::new();
for table in &self.tables {
for field in &table.fields {
if !crate::ddl::needs_own_index(table, field) {
continue;
}
let index = crate::ddl::index_name(&table.name, &field.name);
if let Some((_, first_table, first_field)) =
generated.iter().find(|(name, _, _)| name == &index)
{
errors.push(format!(
"table `{}`, field `{}`: the generated index name `{index}` is already \
generated by table `{first_table}`, field `{first_field}`",
table.name, field.name
));
}
if self.table(&index).is_some() {
errors.push(format!(
"table `{}`, field `{}`: the generated index name `{index}` is also a \
declared table name, and both engines keep indexes and tables in one \
namespace",
table.name, field.name
));
}
generated.push((index, table.name.clone(), field.name.clone()));
}
}
}
fn validate_no_reference_cycle(&self, errors: &mut ConfigError) {
let (_, cyclic) = crate::ddl::creation_order(self);
let Some(first) = cyclic.first() else {
return;
};
let names: Vec<&str> = cyclic.iter().map(|table| table.name.as_str()).collect();
let path = self.walk_cycle(&first.name, &names);
errors.push(format!(
"table `{}`: the foreign keys form a reference cycle, {}, so there is no order that \
creates a table after the tables it references",
first.name,
path.join(" -> ")
));
}
fn walk_cycle(&self, start: &str, unresolved: &[&str]) -> Vec<String> {
let mut path: Vec<&str> = Vec::new();
let mut current = start;
loop {
if let Some(at) = path.iter().position(|name| *name == current) {
let mut cycle: Vec<String> =
path[at..].iter().map(|&name| name.to_owned()).collect();
cycle.push(current.to_owned());
return cycle;
}
path.push(current);
let next = self.table(current).and_then(|table| {
table
.foreign_keys
.iter()
.map(|key| key.references.as_str())
.filter(|name| unresolved.contains(name))
.min()
});
match next {
Some(next) => current = next,
None => return path.iter().map(|&name| name.to_owned()).collect(),
}
}
}
fn validate_table(&self, errors: &mut ConfigError, table: &TableDef) {
if table.fields.is_empty() {
errors.push(format!("table `{}`: declares no fields", table.name));
}
let mut seen_fields: Vec<String> = Vec::new();
for field in &table.fields {
check_identifier(errors, "field", &field.name, &table.name);
if seen_fields.iter().any(|seen| seen == &field.name) {
errors.push(format!(
"table `{}`: field `{}` is declared twice",
table.name, field.name
));
}
seen_fields.push(field.name.clone());
validate_field(errors, table, field);
}
validate_primary_key(errors, table);
for key in &table.foreign_keys {
self.validate_foreign_key(errors, table, key);
}
}
fn validate_foreign_key(&self, errors: &mut ConfigError, table: &TableDef, key: &ForeignKey) {
let Some(local) = table.field(&key.field) else {
errors.push(format!(
"table `{}`: foreign key on `{}`, which is not a declared field",
table.name, key.field
));
return;
};
let Some(target) = self.table(&key.references) else {
errors.push(format!(
"table `{}`: foreign key `{}` references table `{}`, which is not declared",
table.name, key.field, key.references
));
return;
};
let [target_key] = target.primary_key.as_slice() else {
errors.push(format!(
"table `{}`: foreign key `{}` references table `{}`, whose primary key is not a \
single column",
table.name, key.field, target.name
));
return;
};
let Some(target_field) = target.field(target_key) else {
return;
};
if local.kind.as_str() != target_field.kind.as_str() {
errors.push(format!(
"table `{}`: foreign key `{}` is {} but `{}.{}` is {}",
table.name,
key.field,
local.kind.as_str(),
target.name,
target_field.name,
target_field.kind.as_str()
));
}
}
}
fn validate_primary_key(errors: &mut ConfigError, table: &TableDef) {
if table.primary_key.is_empty() {
errors.push(format!("table `{}`: no primary key", table.name));
return;
}
let mut seen: Vec<&String> = Vec::new();
for key in &table.primary_key {
match table.field(key) {
None => errors.push(format!(
"table `{}`: primary key names `{key}`, which is not a declared field",
table.name
)),
Some(field) if !field.required && field.default.is_none() => errors.push(format!(
"table `{}`: primary key `{key}` must be required or have a default, because a \
primary-key column is NOT NULL",
table.name
)),
Some(_) => {}
}
if seen.contains(&key) {
errors.push(format!(
"table `{}`: primary key names `{key}` twice",
table.name
));
}
seen.push(key);
}
}
fn validate_field(errors: &mut ConfigError, table: &TableDef, field: &FieldDef) {
let at = format!("table `{}`, field `{}`", table.name, field.name);
match &field.kind {
FieldKind::Text {
min_len, max_len, ..
} => {
if let (Some(min), Some(max)) = (min_len, max_len)
&& min > max
{
errors.push(format!("{at}: min_len {min} is greater than max_len {max}"));
}
if *max_len == Some(0) {
errors.push(format!("{at}: max_len 0 accepts nothing"));
}
}
FieldKind::Integer { min, max } => {
if let (Some(min), Some(max)) = (min, max)
&& min > max
{
errors.push(format!("{at}: min {min} is greater than max {max}"));
}
}
FieldKind::Real { min, max } => {
if let (Some(min), Some(max)) = (min, max)
&& min > max
{
errors.push(format!("{at}: min {min} is greater than max {max}"));
}
for (label, bound) in [("min", min), ("max", max)] {
if bound.is_some_and(|value| !value.is_finite()) {
errors.push(format!("{at}: {label} is not a finite number"));
}
}
}
FieldKind::Enum { values } => {
if values.is_empty() {
errors.push(format!("{at}: an enum must declare at least one value"));
}
let mut seen: Vec<&String> = Vec::new();
for value in values {
if seen.contains(&value) {
errors.push(format!("{at}: enum value `{value}` is declared twice"));
}
seen.push(value);
if let Some(position) = value.chars().position(char::is_control) {
errors.push(format!(
"{at}: enum value `{}` holds a control character at position {position}",
value.escape_debug()
));
}
}
}
FieldKind::Boolean | FieldKind::Timestamp | FieldKind::Uuid | FieldKind::Json => {}
}
if let Some(text) = field.default.as_ref().and_then(serde_json::Value::as_str)
&& let Some(position) = text.chars().position(char::is_control)
{
errors.push(format!(
"{at}: the default `{}` holds a control character at position {position}",
text.escape_debug()
));
}
if let Some(default) = &field.default
&& let Err(error) = crate::value::check_value(&field.kind, default)
{
errors.push(format!("{at}: the default {default} {}", error.message));
}
if crate::ddl::needs_own_index(table, field) {
let index = crate::ddl::index_name(&table.name, &field.name);
if index.chars().count() > MAX_IDENTIFIER_CHARS {
errors.push(format!(
"{at}: the generated index name `{index}` is longer than \
{MAX_IDENTIFIER_CHARS} characters"
));
}
}
}
fn check_identifier(errors: &mut ConfigError, what: &str, name: &str, at: &str) {
let where_ = if what == "table" {
format!("table `{name}`")
} else {
format!("table `{at}`, field `{name}`")
};
if !is_identifier(name) {
errors.push(format!(
"{where_}: a {what} name must match [a-z][a-z0-9_]* and hold no double or trailing \
underscore"
));
} else if name.chars().count() > MAX_IDENTIFIER_CHARS {
errors.push(format!(
"{where_}: a {what} name is at most {MAX_IDENTIFIER_CHARS} characters"
));
}
if RESERVED_WORDS.contains(&name) {
errors.push(format!("{where_}: `{name}` is a reserved SQL word"));
}
if let Some(prefix) = RESERVED_PREFIXES
.iter()
.find(|prefix| name.starts_with(*prefix))
{
errors.push(format!(
"{where_}: the `{prefix}` prefix is reserved for the harness"
));
}
if let Some(fragment) = cratefield_core::card_data_hit(name) {
errors.push(format!(
"{where_}: `{fragment}` looks like card data. With a normal Stripe integration the \
card never reaches a backend, so store only Stripe's identifiers"
));
}
}
#[must_use]
pub fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_lowercase() {
return false;
}
if name.ends_with('_') || name.contains("__") {
return false;
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}