Skip to content

Commit 45facca

Browse files
Add analyze documentation (#1008)
## Goal Adds analyze documentation to diver core-concepts ## Implementation Also updates driver dependencies to 3.7.0
1 parent 0ece753 commit 45facca

File tree

10 files changed

+353
-3
lines changed

10 files changed

+353
-3
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#tag::analyze[]
2+
with driver.transaction(DB_NAME, TransactionType.READ) as tx:
3+
# 1. Send the analyze request
4+
promise = tx.analyze("""
5+
match { $x isa user; } or { $x isa company; };
6+
fetch { "email": [$x.email] };
7+
""")
8+
9+
# 2. Resolve the promise if you want to access the result or receive an error as an exception
10+
analyzed = promise.resolve()
11+
12+
#end::analyze[]
13+
14+
#tag::get_conjunction[]
15+
pipeline = analyzed.pipeline()
16+
stages = list(pipeline.stages())
17+
block_id = stages[0].as_match().block()
18+
root_conjunction = pipeline.conjunction(block_id)
19+
20+
#end::get_conjunction[]
21+
22+
#tag::get_first_branch_isa[]
23+
constraints = list(root_conjunction.constraints())
24+
or_constraint = constraints[0]
25+
assert or_constraint.is_or()
26+
branches = [pipeline.conjunction(id) for id in or_constraint.as_or().branches()]
27+
first_branch_constraints = list(branches[0].constraints())
28+
first_branch_isa = first_branch_constraints[0]
29+
assert first_branch_isa.is_isa()
30+
31+
#end::get_first_branch_isa[]
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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]

core-concepts/modules/ROOT/pages/drivers/index.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ Using transactions with TypeDB drivers.
3535
Querying with TypeDB drivers.
3636
****
3737

38+
.xref:{page-version}@core-concepts::drivers/analyze.adoc[]
39+
[.clickable]
40+
****
41+
Type-checking queries without executing them against data.
42+
****
43+
3844
.xref:{page-version}@core-concepts::drivers/best-practices.adoc[]
3945
[.clickable]
4046
****

core-concepts/modules/ROOT/partials/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@
3131
** xref:{page-version}@core-concepts::drivers/authentication.adoc[]
3232
** xref:{page-version}@core-concepts::drivers/transactions.adoc[]
3333
** xref:{page-version}@core-concepts::drivers/queries.adoc[]
34+
** xref:{page-version}@core-concepts::drivers/analyze.adoc[]
3435
** xref:{page-version}@core-concepts::drivers/best-practices.adoc[]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
typedb-driver==3.4.0
1+
typedb-driver==3.7.0

test/code/runners/rust_cargo_toml.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ edition = "2021"
55

66
[dependencies]
77
serde_json = "1.0.114"
8-
typedb-driver = { version = "3.4.0" }
8+
typedb-driver = { version = "3.7.0" }
99
tokio = "1.43.0"
1010
futures-util = "0.3.31"
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
name: external-typedb-driver
22
title: Mock driver
33
version: '3.x'
4-
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
== Analyze
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
== Analyze
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
== Analyze

0 commit comments

Comments
 (0)