use crate::error::{TViewError, TViewResult};
use regex::Regex;
use std::sync::LazyLock;
static CREATE_TVIEW_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?ix) # Case-insensitive, verbose
CREATE\s+TABLE\s+ # CREATE TABLE keyword
(?:(\w+)\.)? # Optional schema name
(\w+) # Table name (required)
\s+AS\s+ # AS keyword
(.+) # SELECT statement (rest of query)
",
)
.expect("CREATE_TVIEW_REGEX pattern is valid")
});
fn get_create_tview_regex() -> &'static Regex {
&CREATE_TVIEW_REGEX
}
#[derive(Debug, Clone)]
pub struct CreateTViewStmt {
pub tview_name: String,
pub schema_name: Option<String>,
pub select_sql: String,
}
pub fn parse_create_tview(sql: &str) -> TViewResult<CreateTViewStmt> {
let re = get_create_tview_regex();
let caps = re
.captures(sql.trim())
.ok_or_else(|| TViewError::InvalidSelectStatement {
sql: sql.to_string(),
reason: "Could not parse CREATE TABLE tv_ AS SELECT statement. \
Syntax: CREATE TABLE tv_name AS SELECT ...\n\
See docs for limitations."
.to_string(),
})?;
let schema_name = caps.get(1).map(|m| m.as_str().to_string());
let tview_name = caps
.get(2)
.ok_or_else(|| TViewError::InvalidTViewName {
name: String::new(),
reason: "Missing TVIEW name".to_string(),
})?
.as_str()
.to_string();
let select_sql = caps
.get(3)
.ok_or_else(|| TViewError::InvalidSelectStatement {
sql: sql.to_string(),
reason: "Missing SELECT statement after AS".to_string(),
})?
.as_str()
.trim()
.to_string();
if !tview_name.starts_with("tv_") {
return Err(TViewError::InvalidTViewName {
name: tview_name,
reason: "TVIEW name must start with 'tv_'".to_string(),
});
}
if !select_sql.to_uppercase().starts_with("SELECT") {
return Err(TViewError::InvalidSelectStatement {
sql: select_sql,
reason: "Expected SELECT statement after AS".to_string(),
});
}
if select_sql.to_uppercase().contains(" WITH ") {
pgrx::warning!("CTEs (`WITH` clause) may not be fully supported in v1");
}
if select_sql.contains("/*") || select_sql.contains("--") {
pgrx::warning!("Comments in SELECT may cause parsing issues in v1");
}
Ok(CreateTViewStmt {
tview_name,
schema_name,
select_sql,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let sql = "CREATE TABLE tv_post AS SELECT * FROM tb_post";
let parsed = parse_create_tview(sql).unwrap();
assert_eq!(parsed.tview_name, "tv_post");
assert!(parsed.schema_name.is_none());
assert!(parsed.select_sql.contains("SELECT"));
}
#[test]
fn test_parse_with_schema() {
let sql = "CREATE TABLE public.tv_post AS SELECT pk_post FROM tb_post";
let parsed = parse_create_tview(sql).unwrap();
assert_eq!(parsed.tview_name, "tv_post");
assert_eq!(parsed.schema_name, Some("public".to_string()));
}
#[test]
fn test_parse_multiline() {
let sql = r"
CREATE TABLE tv_post AS
SELECT
pk_post,
id,
data
FROM tb_post
";
let parsed = parse_create_tview(sql).unwrap();
assert_eq!(parsed.tview_name, "tv_post");
assert!(parsed.select_sql.contains("pk_post"));
}
#[test]
fn test_parse_invalid_name() {
let sql = "CREATE TABLE bad_name AS SELECT * FROM tb";
let result = parse_create_tview(sql);
assert!(result.is_err());
match result.unwrap_err() {
TViewError::InvalidTViewName { name, .. } => {
assert_eq!(name, "bad_name");
}
_ => panic!("Wrong error type"),
}
}
#[test]
fn test_create_tview_regex_cached() {
let re = get_create_tview_regex();
let sql = "CREATE TABLE tv_user AS SELECT id, data FROM tb_user";
assert!(
re.is_match(sql),
"Cached regex should match CREATE TABLE statement"
);
let caps = re.captures(sql);
assert!(caps.is_some(), "Should capture groups from CREATE TABLE");
let caps = caps.unwrap();
assert_eq!(
caps.get(2).map(|m| m.as_str()),
Some("tv_user"),
"Should capture table name"
);
}
}