Line data Source code
1 : //! The relations that statements create, change or drop.
2 : //!
3 : //! The catalog of a connection holds what the connection reports. After a successful statement the host
4 : //! asks the connection about the relations that the statement touched, the parser names them. A text
5 : //! that doesn't parse touches nothing that we know of.
6 :
7 : use dashql_buffers::analyzer::QualifiedTableNameT;
8 : use dashql_buffers::catalog::TouchedRelationsT;
9 : use dashql_buffers::parser::{AttributeKey, Node, NodeType, StatementType};
10 : use dashql_core::catalog::Catalog;
11 : use dashql_core::pack::{pack_parsed_statement, read_name_path};
12 : use dashql_core::script::Script;
13 : use dashql_parser::parser::ParsedScript;
14 : use dashql_parser::parser::context::resolve_text_span;
15 :
16 : /// A relation name from its parts, an empty part is not named
17 778 : fn relation_name(database: Option<String>, schema: Option<String>, table: String) -> QualifiedTableNameT {
18 778 : let mut name = QualifiedTableNameT::default();
19 778 : name.database_name = database;
20 778 : name.schema_name = schema;
21 778 : name.table_name = Some(table);
22 778 : name
23 778 : }
24 :
25 : /// Do the options of an `EXPLAIN` run its statement? Unknown arguments of `analyze` count as running it.
26 80 : pub fn explain_executes(parsed: &ParsedScript, options: Option<&Node>) -> bool {
27 80 : let scanned = &parsed.scanned;
28 80 : let option_text = |n: Option<&Node>| -> String {
29 38 : let Some(n) = n.filter(|n| n.node_type() != NodeType::NONE) else { return String::new() };
30 38 : let span = resolve_text_span(&scanned.text, &scanned.symbols, *n.symbol_span());
31 38 : let text = scanned.text[span.offset as usize..(span.offset + span.length) as usize].to_ascii_lowercase();
32 38 : let b = text.as_bytes();
33 38 : if b.len() >= 2 && (b[0] == b'\'' || b[0] == b'"') && b[b.len() - 1] == b[0] {
34 3 : return text[1..text.len() - 1].to_string();
35 35 : }
36 35 : text
37 38 : };
38 80 : let mut executes = false;
39 80 : for option in options.filter(|o| o.node_type() == NodeType::ARRAY).iter().flat_map(|o| parsed.children(o)) {
40 35 : if option.node_type() != NodeType::OBJECT_SQL_GENERIC_OPTION {
41 0 : continue;
42 35 : }
43 35 : let key = attribute_of(parsed, option, AttributeKey::SQL_GENERIC_OPTION_KEY);
44 35 : let value = attribute_of(parsed, option, AttributeKey::SQL_GENERIC_OPTION_VALUE);
45 35 : let name = option_text(key);
46 35 : if name == "analyze" || name == "analyse" {
47 26 : executes |= value.is_none_or(|v| v.node_type() != NodeType::BOOL || v.children_begin_or_value() != 0);
48 9 : } else if name == "format" && matches!(option_text(value).as_str(), "analyze" | "analyse") {
49 3 : executes = true;
50 6 : }
51 : }
52 80 : executes
53 80 : }
54 :
55 : /// The child of a node with an attribute key
56 140 : fn attribute_of<'a>(parsed: &'a ParsedScript, node: &Node, key: AttributeKey) -> Option<&'a Node> {
57 216 : parsed.children(node).iter().find(|c| c.attribute_key() == key)
58 140 : }
59 :
60 : /// A name from the parts of a name path, an empty table names a whole schema
61 8 : fn name_of_path(path: &[String], database: &Option<String>, schema: &Option<String>) -> Option<QualifiedTableNameT> {
62 8 : match path {
63 6 : [name] => Some(relation_name(database.clone(), schema.clone(), name.clone())),
64 2 : [new_schema, name] => Some(relation_name(database.clone(), Some(new_schema.clone()), name.clone())),
65 0 : [new_database, new_schema, name] => Some(relation_name(Some(new_database.clone()), Some(new_schema.clone()), name.clone())),
66 0 : _ => None,
67 : }
68 8 : }
69 :
70 : /// The relations that a statement touches, named as written. A statement that moves a relation touches its old and its
71 : /// new name, one that changes a schema touches everything in it: its name has an empty table. Empty for other statements
72 : /// and for a statement whose relation the parser could not name.
73 1209 : pub fn touched_relations_of_statement(parsed: &ParsedScript, statement_id: usize) -> Vec<QualifiedTableNameT> {
74 1209 : let Some(statement) = parsed.statements.get(statement_id) else { return Vec::new() };
75 1209 : let root = &parsed.nodes[statement.root as usize];
76 1209 : let read = |node: &Node| read_name_path(parsed, node);
77 1213 : let attribute = |node: &Node, key: AttributeKey| attribute_of(parsed, node, key);
78 1209 : match statement.statement_type {
79 : StatementType::CREATE_TABLE
80 : | StatementType::CREATE_TABLE_AS
81 : | StatementType::CREATE_VIEW
82 : | StatementType::SELECT_INTO
83 : | StatementType::DROP_TABLE
84 : | StatementType::DROP_VIEW
85 : | StatementType::ALTER_TABLE
86 758 : | StatementType::ALTER_VIEW => {}
87 : StatementType::DROP_SCHEMA | StatementType::ALTER_SCHEMA => {
88 6 : let Some(path) = attribute(root, AttributeKey::SQL_SCHEMA_NAME).map(read) else { return Vec::new() };
89 6 : let (database, schema) = match path.as_slice() {
90 5 : [schema] => (None, schema.clone()),
91 1 : [database, schema] => (Some(database.clone()), schema.clone()),
92 0 : _ => return Vec::new(),
93 : };
94 6 : let mut out = vec![relation_name(database.clone(), Some(schema), String::new())];
95 6 : if let Some([new_schema]) = attribute(root, AttributeKey::SQL_SCHEMA_NEW_NAME).map(read).as_deref() {
96 2 : out.push(relation_name(database, Some(new_schema.clone()), String::new()));
97 4 : }
98 6 : return out;
99 : }
100 : // A statement that `EXPLAIN` runs creates its table
101 : StatementType::EXPLAIN => {
102 5 : let options = attribute(root, AttributeKey::EXT_EXPLAIN_OPTIONS);
103 5 : let body = attribute(root, AttributeKey::EXT_EXPLAIN_STATEMENT);
104 5 : let Some(body) = body.filter(|b| b.node_type() == NodeType::OBJECT_SQL_CREATE_AS && explain_executes(parsed, options)) else { return Vec::new() };
105 1 : let Some(path) = attribute(body, AttributeKey::SQL_CREATE_AS_NAME).map(read) else { return Vec::new() };
106 1 : return name_of_path(&path, &None, &None).into_iter().collect();
107 : }
108 440 : _ => return Vec::new(),
109 : }
110 758 : let Some(target) = pack_parsed_statement(parsed, statement_id).target else { return Vec::new() };
111 758 : let Some(table) = target.relation_name.clone().filter(|n| !n.is_empty()) else { return Vec::new() };
112 758 : let (database, schema) = (target.database_name.clone(), target.schema_name.clone());
113 758 : let mut out = vec![relation_name(database.clone(), schema.clone(), table.clone())];
114 758 : if matches!(statement.statement_type, StatementType::ALTER_TABLE | StatementType::ALTER_VIEW) {
115 36 : let actions = attribute(root, AttributeKey::SQL_ALTER_TABLE_ACTIONS);
116 40 : for action in actions.iter().flat_map(|actions| parsed.children(actions)) {
117 40 : match action.node_type() {
118 : // A new name without a schema stays where the relation is
119 : NodeType::OBJECT_SQL_ALTER_RENAME_TABLE => {
120 7 : if let Some(path) = attribute(action, AttributeKey::SQL_ALTER_ACTION_NEW_TABLE).map(read) {
121 7 : out.extend(name_of_path(&path, &database, &schema));
122 7 : }
123 : }
124 : NodeType::OBJECT_SQL_ALTER_SET_SCHEMA => {
125 4 : if let Some([new_schema]) = attribute(action, AttributeKey::SQL_ALTER_ACTION_NEW_NAME).map(read).as_deref() {
126 4 : out.push(relation_name(database.clone(), Some(new_schema.clone()), table.clone()));
127 4 : }
128 : }
129 29 : _ => {}
130 : }
131 : }
132 722 : }
133 758 : out
134 1209 : }
135 :
136 : /// The relations that the statements of a text touch
137 48 : pub fn touched_relations(text: &str) -> TouchedRelationsT {
138 48 : let mut script = Script::new(&Catalog::new());
139 48 : script.replace_text(text);
140 48 : script.parse();
141 48 : let mut touched = TouchedRelationsT::default();
142 48 : let Some(parsed) = script.parsed.as_ref() else {
143 0 : return touched;
144 : };
145 51 : touched.relations = Some((0..parsed.statements.len()).flat_map(|id| touched_relations_of_statement(parsed, id)).collect());
146 48 : touched
147 48 : }
|