|
| 1 | += Analyzing queries |
| 2 | +:pageTitle: Analyzing queries |
| 3 | +:Summary: Compiling & analyzing TypeDB queries. |
| 4 | +:keywords: typedb, driver, analyze, type-inference, dry-run, validate |
| 5 | +:test-python: true |
| 6 | + |
| 7 | +TypeDB allows the user to "analyze" a query without having it execute against the data. |
| 8 | +During analysis, a query is parsed to the internal representation and type-checked - |
| 9 | +allowing the user to check their query for syntax and typing errors against a schema. |
| 10 | + |
| 11 | +The envisioned use is to facilitate developer tooling around TypeDB, such as |
| 12 | +plugins to validate TypeQL queries when application code is being compiled, |
| 13 | +and debugging type errors. |
| 14 | +Such tools can also be used by AI query-generators to automatically validate generated queries. |
| 15 | + |
| 16 | +This page gives an overview of how to analyze a query and use the response. |
| 17 | +The examples are valid python code using the TypeDB python driver. |
| 18 | +The class definitions are illustrative. |
| 19 | + |
| 20 | +== Analyzing a query |
| 21 | +Analyzing a query follows the same pattern as xref:{page-version}@core-concepts::drivers/queries.adoc[running a query]. |
| 22 | +[source,python] |
| 23 | +---- |
| 24 | +#!test[] |
| 25 | +#{{ |
| 26 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=setup_and_schema] |
| 27 | +
|
| 28 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=data_create] |
| 29 | +
|
| 30 | +#}} |
| 31 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tag=analyze] |
| 32 | +---- |
| 33 | + |
| 34 | +== The response |
| 35 | + |
| 36 | +The response is a type-annotated representation of the query. |
| 37 | +The following sections cover the essence, |
| 38 | +but details are left to the xref:#_references[driver reference]. |
| 39 | + |
| 40 | +As one would expect, the response contains each part of a TypeQL query: |
| 41 | +A (possibly empty) set of preamble functions, the query-pipeline, and a fetch clause, if present. |
| 42 | + |
| 43 | +[source,python] |
| 44 | +---- |
| 45 | +class AnalyzedQuery: |
| 46 | + def pipeline(self) -> Pipeline |
| 47 | + def preamble(self) -> Iterator[Function] |
| 48 | + def fetch(self) -> Optional[Fetch] |
| 49 | +---- |
| 50 | + |
| 51 | +=== Pipelines & conjunctions |
| 52 | +TypeQL xref:{page-version}@typeql-reference::/data-model.adoc#_stages[pipelines] are made up of a sequence of stages, some of which may contain conjunctions. |
| 53 | + |
| 54 | +[source,python] |
| 55 | +---- |
| 56 | +class Pipeline: |
| 57 | + def stages(self) -> Iterator[PipelineStage] |
| 58 | + def conjunction(self, conjunction_id: ConjunctionID) -> Optional[Conjunction] |
| 59 | + # ... |
| 60 | +---- |
| 61 | +The `PipelineStage` instances returned by the `stages()` method follows the familiar pattern |
| 62 | +of having an abstract base class which is a union of all the variants. |
| 63 | +it must be downcast to the appropriate variant using the `is_<variant>` and `as_<variant>` methods. |
| 64 | + |
| 65 | +[source,python] |
| 66 | +---- |
| 67 | +class PipelineStage(ABC): |
| 68 | + def is_match(self) -> bool |
| 69 | + def as_match(self) -> MatchStage |
| 70 | + # is_insert, as_insert, is_select, as_select, ... |
| 71 | +
|
| 72 | +class MatchStage(PipelineStage): |
| 73 | + def block(self) -> ConjunctionID |
| 74 | +
|
| 75 | +class SelectStage(PipelineStage): |
| 76 | + def variables(self) -> Iterator[Variable] |
| 77 | +---- |
| 78 | + |
| 79 | +Stages such as `Match` and `Insert` hold a `ConjunctionID`. |
| 80 | +This is an indirection which can be used |
| 81 | +to retrieve the actual conjunction using the `Pipeline.conjunction` method. |
| 82 | + |
| 83 | +[source,python] |
| 84 | +---- |
| 85 | +#!test[] |
| 86 | +#{{ |
| 87 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=import_and_constants] |
| 88 | +
|
| 89 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=driver_create] |
| 90 | +
|
| 91 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tag=analyze] |
| 92 | +
|
| 93 | +#}} |
| 94 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tag=get_conjunction] |
| 95 | +---- |
| 96 | + |
| 97 | +From the returned conjunction one can access the constraints, |
| 98 | +as well as the types inferred for the variables in those constraints. |
| 99 | +[source,python] |
| 100 | +---- |
| 101 | +class Conjunction: |
| 102 | + def constraints(self) -> Iterator[Constraint] |
| 103 | + def annotated_variables(self) -> Iterator[Variable] |
| 104 | + def variable_annotations(self, variable: Variable) -> Optional[VariableAnnotations] |
| 105 | +---- |
| 106 | + |
| 107 | +[NOTE] |
| 108 | +==== |
| 109 | +`VariableAnnotations` refer to the types the variable is annotated with by type-inference. |
| 110 | +These are not to be confused with xref:{page-version}@typeql-reference::annotations/index.adoc[schema-annotations] |
| 111 | +==== |
| 112 | + |
| 113 | +=== Constraints |
| 114 | +Similar to stages, the `Constraint` instances returned by the `constraints()` method |
| 115 | +must be down-cast to the appropriate variant using the `is/as` methods. |
| 116 | +Sub-patterns such as `or`, `not`, and `try` are also constraints. |
| 117 | +These hold the `ConjunctionID`(s) of the nested conjunctions. |
| 118 | + |
| 119 | +[source,python] |
| 120 | +---- |
| 121 | +class Constraint(ABC): |
| 122 | + def is_isa(self) -> bool |
| 123 | + def as_isa(self) -> Isa |
| 124 | + # is_has, as_has, ... |
| 125 | +
|
| 126 | + def is_or(self) -> bool |
| 127 | + def as_or(self) -> Or |
| 128 | + # is_not, as_not, is_try, as_try |
| 129 | +
|
| 130 | +class Isa(Constraint): |
| 131 | + # <instance> isa(!) <type> |
| 132 | + def instance(self) -> ConstraintVertex |
| 133 | + def type(self) -> ConstraintVertex |
| 134 | + def exactness(self) -> ConstraintExactness # isa or isa! |
| 135 | +
|
| 136 | +# Has, ... |
| 137 | +
|
| 138 | +class Or(Constraint): |
| 139 | + def branches(self) -> Iterator[ConjunctionID] |
| 140 | +# Not, Try |
| 141 | +---- |
| 142 | + |
| 143 | +To get the `isa` constraint from the first branch: |
| 144 | +[source,python] |
| 145 | +---- |
| 146 | +#!test[] |
| 147 | +#{{ |
| 148 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=import_and_constants] |
| 149 | +
|
| 150 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=driver_create] |
| 151 | +
|
| 152 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tags=analyze;get_conjunction] |
| 153 | +
|
| 154 | +#}} |
| 155 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tag=get_first_branch_isa] |
| 156 | +---- |
| 157 | + |
| 158 | +==== Constraint vertices |
| 159 | +Although constraints typically apply on variables, |
| 160 | +certain TypeQL constraints allow you to directly specify a type-label or a value. |
| 161 | +Additionally, a `NamedRole` vertex type exists to handle the ambiguity of unscoped role-labels. |
| 162 | +A `ConstraintVertex` is the union of these four. |
| 163 | + |
| 164 | +[NOTE] |
| 165 | +==== |
| 166 | +The term `vertex` comes from viewing a query as a constraint graph. |
| 167 | +==== |
| 168 | + |
| 169 | +A `ConstraintVertex` can be converted to the appropriate variant using the `is/as` methods. |
| 170 | + |
| 171 | +* A *label* vertex holds the resolved type. |
| 172 | +* A *value* vertex holds the value concept of the appropriate value-type. |
| 173 | +* A *named-role* vertex holds the internal variable and the unscoped name. |
| 174 | +The internal variable can be used to retrieve the resolved role-type(s) using the annotations in the conjunction. |
| 175 | +* A *variable* vertex holds a variable, which can be used in many places. |
| 176 | + |
| 177 | +A variable is shared across constraints. The name of a variable (if it has one) can be read from the pipeline. |
| 178 | +[source,python] |
| 179 | +---- |
| 180 | +#!test[] |
| 181 | +#{{ |
| 182 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=import_and_constants] |
| 183 | +
|
| 184 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=driver_create] |
| 185 | +
|
| 186 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tags=analyze;get_conjunction;get_first_branch_isa] |
| 187 | +
|
| 188 | +#}} |
| 189 | +var_x = first_branch_isa.instance() |
| 190 | +assert var_x.is_variable() and pipeline.get_variable_name(var_x.as_variable()) == "x" |
| 191 | +---- |
| 192 | +If the variable is an output of the pipeline, the name can be used to read answers from query responses. |
| 193 | +The possible types of a variable in a conjunction can be read from the annotations in the conjunction. |
| 194 | + |
| 195 | +[NOTE] |
| 196 | +==== |
| 197 | +`Variable` and `ConjunctionID` are scoped to a pipeline. |
| 198 | +Trying to resolve either of these using a pipeline other than the one it originated from |
| 199 | +(e.g. a pipeline of a preamble function) is undefined behaviour. |
| 200 | +==== |
| 201 | + |
| 202 | +=== Annotations |
| 203 | +Type-checking is a central feature of TypeDB. |
| 204 | +Analyze returns the final set of inferred types for every variable in a conjunction. |
| 205 | + |
| 206 | +[source,python] |
| 207 | +---- |
| 208 | +#!test[] |
| 209 | +#{{ |
| 210 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=import_and_constants] |
| 211 | +
|
| 212 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=driver_create] |
| 213 | +
|
| 214 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tags=analyze;get_conjunction;get_first_branch_isa] |
| 215 | +
|
| 216 | +#}} |
| 217 | +var_x = first_branch_isa.instance().as_variable() |
| 218 | +assert var_x in list(root_conjunction.annotated_variables()) |
| 219 | +
|
| 220 | +x_annotations_in_root = root_conjunction.variable_annotations(var_x) |
| 221 | +assert x_annotations_in_root.is_instance() |
| 222 | +
|
| 223 | +x_types_in_root = list(x_annotations_in_root.as_instance()) |
| 224 | +labels = set(map(lambda t: t.get_label(), x_types_in_root)) |
| 225 | +assert labels == {"user", "company"} |
| 226 | +---- |
| 227 | + |
| 228 | +[NOTE] |
| 229 | +==== |
| 230 | +We return annotations per conjunction because a variable may have different types in different conjunctions. |
| 231 | +[source, typeql] |
| 232 | +---- |
| 233 | +# user in the left branch, company in the right, Either of them at the root. |
| 234 | +match { $p isa user; } or { $p isa company; }; |
| 235 | +---- |
| 236 | +or |
| 237 | +[source, typeql] |
| 238 | +---- |
| 239 | +match $p has email $email; # $p is any type that owns email |
| 240 | +match $p has name $name; # The type of $p must also own name |
| 241 | +---- |
| 242 | +==== |
| 243 | + |
| 244 | + |
| 245 | +=== Functions |
| 246 | +A `Function` is a pipeline with a set of arguments, and returns. |
| 247 | +[source, python] |
| 248 | +---- |
| 249 | +class Function: |
| 250 | + def body(self) -> Pipeline |
| 251 | +
|
| 252 | + def argument_variables(self) -> Iterator[Variable] |
| 253 | + def argument_annotations(self) -> Iterator[VariableAnnotations] |
| 254 | +
|
| 255 | + def return_operation(self) -> ReturnOperation |
| 256 | + def return_annotations(self) -> Iterator[VariableAnnotations] |
| 257 | +---- |
| 258 | + |
| 259 | +=== Fetch |
| 260 | +An analyzed `Fetch` is one of a Dictionary, a List, or a collection of values. |
| 261 | +[source,python] |
| 262 | +---- |
| 263 | +class Fetch(ABC): |
| 264 | + # is/as methods |
| 265 | +
|
| 266 | +class FetchObject(Fetch): |
| 267 | + def keys(self) -> Iterator[str] |
| 268 | + def get(self, key: str) -> Fetch |
| 269 | +
|
| 270 | +class FetchList(Fetch): |
| 271 | + def element(self) -> Fetch |
| 272 | +
|
| 273 | +class FetchLeaf(Fetch): |
| 274 | + def annotations(self) -> Iterator[str] |
| 275 | +---- |
| 276 | +The JSON-like structure of the analyzed `Fetch` reflects that of the `Fetch` stage itself, |
| 277 | +with leaves being annotated with the value types. |
| 278 | + |
| 279 | +[source,typeql] |
| 280 | +---- |
| 281 | +match $u isa user; |
| 282 | +fetch { |
| 283 | + "email": [ $u.email ], |
| 284 | +}; |
| 285 | +---- |
| 286 | + |
| 287 | +To inspect the value-type of the emails: |
| 288 | +[source,python] |
| 289 | +---- |
| 290 | +#!test[] |
| 291 | +#{{ |
| 292 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=import_and_constants] |
| 293 | +
|
| 294 | +include::{page-version}@core-concepts::example$driver/python_driver_usage.py[tag=driver_create] |
| 295 | +
|
| 296 | +include::{page-version}@core-concepts::example$driver/analyze_query.py[tag=analyze] |
| 297 | +
|
| 298 | +#}} |
| 299 | +root_object = analyzed.fetch().as_object() |
| 300 | +email_field = root_object.get("email") |
| 301 | +email_list_element = email_field.as_list().element() |
| 302 | +email_value_types = list(email_list_element.annotations()) |
| 303 | +assert email_value_types == ["string"] |
| 304 | +---- |
| 305 | + |
| 306 | +== References |
| 307 | +* Analyze reference: |
| 308 | +xref:{page-version}@reference::typedb-grpc-drivers/rust.adoc#_Analyze[rust], |
| 309 | +xref:{page-version}@reference::typedb-grpc-drivers/java.adoc#_Analyze[java], |
| 310 | +xref:{page-version}@reference::typedb-grpc-drivers/python.adoc#_Analyze[python] |
0 commit comments