From c67a46cba802314769b30582e023e6597afa2a62 Mon Sep 17 00:00:00 2001 From: joshua Date: Thu, 13 Nov 2025 18:34:58 -0500 Subject: [PATCH 1/4] Add sql vs typeql page --- .../ROOT/pages/typeql/sql-vs-typeql.adoc | 718 ++++++++++++++++++ core-concepts/modules/ROOT/partials/nav.adoc | 2 + 2 files changed, 720 insertions(+) create mode 100644 core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc diff --git a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc new file mode 100644 index 000000000..957c03c9d --- /dev/null +++ b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc @@ -0,0 +1,718 @@ += SQL versus TypeQL guide +:test-typeql: linear + +This guide helps SQL users understand TypeQL by mapping familiar SQL constructs to their TypeQL equivalents. +TypeQL is a declarative query language for TypeDB that uses pattern matching rather than table joins. + +[NOTE] +==== +TypeQL operates on a graph data model (entities, relations, and attributes) rather than tables and rows. +This fundamental difference means that some SQL concepts map directly, while others require a different approach. +==== + +== Schema Setup + +For the examples in this guide, we'll use a simple schema: + +[,typeql] +---- +#!test[schema] +define + entity person, owns name, owns email, owns age; + attribute name, value string; + attribute email, value string; + attribute age, value integer; + relation employment, relates employee, relates employer; + person plays employment:employee; + entity company, owns name, plays employment:employer; + company owns name; +#!test[write, commit] +insert + $alice isa person, has name "Alice", has email "alice@example.com", has age 30; + $bob isa person, has name "Bob", has email "bob@example.com", has age 25; + $charlie isa person, has name "Charlie", has email "charlie@example.com", has age 35; + $acme isa company, has name "Acme Corp"; + $e1 isa employment, links (employee: $alice, employer: $acme); +---- +==== + +== Basic Query Operations + +=== SELECT → match + fetch/select + +In SQL, `SELECT` retrieves data from tables. In TypeQL, `match` finds data by pattern, and `fetch` or `select` projects the results. + +**SQL:** +[source,sql] +---- +SELECT name, email FROM person; +---- + +**TypeQL:** + +In TypeQL, use SELECT to filter out unwanted data: + +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $name, has email $email; +select $name, $email; # eliminates $p +---- + +Special case: if you're returning data and want to format it through a `fetch` clause, it can also work as a projection: + +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $name, has email $email; +fetch { + "name": $name, + "email": $email +}; +---- + +=== WHERE → Pattern Constraints + +SQL's `WHERE` clause filters rows. In TypeQL, constraints are part of the `match` pattern. + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE age > 25; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=2] +match + $p isa person, has name $name, has age $age; + $age > 25; +select $name; +---- + +Shorthand: +[,typeql] +---- +#!test[read, count=2] +match + $p isa person, has name $name, has age > 25; +select $name; +---- + + +==== AND/OR Conditions in WHERE clauses + +SQL uses `AND` and `OR` in `WHERE` clauses. TypeQL uses pattern conjunction (comma or semicolon) and disjunction (`or`). + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE age > 25 AND email LIKE '%@example.com'; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=2] +match + $p isa person, has name $name, has age $age, has email $email; + $age > 25; + $email like ".*@example.com"; +select $name; +---- + +**SQL with OR:** +[source,sql] +---- +SELECT name FROM person WHERE age < 20 OR age > 30; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=1] +match + $p isa person, has name $name, has age $age; + { $age < 20; } or { $age > 30; }; +select $name; +---- + +=== JOIN → Relation Patterns + +SQL uses `JOIN` to combine data from multiple tables. In TypeQL, relations naturally connect entities through patterns. +The type of JOIN determines which entities are included in the results when relationships don't exist. + +==== INNER JOIN (or JOIN) + +An INNER JOIN returns only rows where there is a match in both tables. In TypeQL, this is the default behavior when matching relations. + +**SQL:** +[source,sql] +---- +SELECT p.name, e.id +FROM person p +INNER JOIN employment e ON p.id = e.employee_id; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=1] +match + $p isa person, has name $pname; + $e isa employment, links (employee: $p); +select $pname, $e; +---- + +This returns only persons who have an employment relation. + +==== LEFT JOIN + +A LEFT JOIN returns all rows from the left table (person), and matching rows from the right table (employment). +If there's no match, NULL values are returned for the right table columns. In TypeQL, use the `try` construct for optional matching. + +**SQL:** +[source,sql] +---- +SELECT p.name, e.id +FROM person p +LEFT JOIN employment e ON p.id = e.employee_id; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $pname; + try { + $e isa employment, links (employee: $p); + }; +select $pname, $e; +---- + +This returns all persons, with their employment if they have one. +Persons without employment will still appear in the results (with `$e` unbound). + +==== RIGHT JOIN + +A RIGHT JOIN returns all rows from the right table (employment), and matching rows from the left table (person). +If there's no match, NULL values are returned for the left table columns. In TypeQL, reverse the pattern to start from the right side. + +**SQL:** +[source,sql] +---- +SELECT p.name, e.id +FROM person p +RIGHT JOIN employment e ON p.id = e.employee_id; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=1] +match + $e isa employment; + try { + $p isa person, has name $pname; + $e links (employee: $p); + }; +select $pname; +---- + +This returns all employments, with employee names if they exist. +Employments without matching persons will still appear in the results (with `$pname` unbound). + +==== CROSS JOIN + +A CROSS JOIN returns the Cartesian product of two tables - every row from the first table combined with every row from the second table. +In TypeQL, match entities from both types independently without requiring any relation between them. + +**SQL:** +[source,sql] +---- +SELECT p.name, e.id +FROM person p +CROSS JOIN employment e; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $pname; + $e isa employment; +select $pname, $e; +---- + +This returns every combination of person and employment, regardless of whether they have any relationship. +The result set size is the product of the number of persons and employments. + +==== FULL OUTER JOIN + +A FULL OUTER JOIN returns all rows from both tables, with NULL values where there's no match. + +TypeQL doesn't yet have an efficient equivalent of full outer joins! Please leave us a message if this operation is required for your application. + +== Data Modification + +=== INSERT → insert + +**SQL:** +[source,sql] +---- +INSERT INTO person (name, email, age) VALUES ('David', 'david@example.com', 28); +---- + +**TypeQL:** +[,typeql] +---- +#!test[write, commit, count=1] +insert + $p isa person, has name "David", has email "david@example.com", has age 28; +---- + +**SQL INSERT with SELECT:** +[source,sql] +---- +INSERT INTO employment (employee_id, employer_id) +SELECT p.id, c.id +FROM person p, company c +WHERE p.name = 'Alice' AND c.name = 'Acme Corp'; +---- + +**TypeQL:** +[,typeql] +---- +#!test[write, commit, count=1] +match + $p isa person, has name "Alice"; + $c isa company, has name "Acme Corp"; +insert + $e isa employment, links (employee: $p, employer: $c); +---- + +=== UPDATE → update + +**SQL:** +[source,sql] +---- +UPDATE person SET email = 'newemail@example.com' WHERE name = 'Alice'; +---- + +**TypeQL:** +[,typeql] +---- +#!test[write, count=1] +match + $p isa person, has name "Alice"; +update $p has email "newemail@example.com"; +---- + +[NOTE] +==== +The `update` clause in TypeQL works only for attributes with cardinality `@card(0..1)` or `@card(1)`. +It automatically deletes the old value and inserts the new one. +==== + +=== DELETE → delete + +**SQL:** +[source,sql] +---- +DELETE FROM person WHERE name = 'Charlie'; +---- + +**TypeQL:** +[,typeql] +---- +#!test[write, rollback, count=1] +match + $p isa person, has name "Charlie"; +delete $p; +---- + +**SQL DELETE with JOIN:** +[source,sql] +---- +DELETE e FROM employment e +INNER JOIN person p ON e.employee_id = p.id +WHERE p.name = 'Alice'; +---- + +**TypeQL:** +[,typeql] +---- +#!test[write, rollback, count=1] +match + $p isa person, has name "Alice"; + $e isa employment, links (employee: $p); +delete $e; +---- + +== Aggregation and Grouping + +=== GROUP BY → reduce with groupby + +**SQL:** +[source,sql] +---- +SELECT age, COUNT(*) as count +FROM person +GROUP BY age; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has age $age; +reduce $count = count groupby $age; +---- + +=== Aggregate Functions + +**SQL:** +[source,sql] +---- +SELECT + COUNT(*) as total, + AVG(age) as avg_age, + MAX(age) as max_age, + MIN(age) as min_age, + SUM(age) as sum_age +FROM person; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=1] +match + $p isa person, has age $age; +reduce + $total = count, + $avg_age = mean($age), + $max_age = max($age), + $min_age = min($age), + $sum_age = sum($age); +---- + +=== HAVING → reduce + match + +**SQL:** +[source,sql] +---- +SELECT age, COUNT(*) as count +FROM person +GROUP BY age +HAVING COUNT(*) > 1; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=0] +match + $p isa person, has age $age; +reduce $count = count groupby $age; +match $count > 1; +---- + +== Sorting and Pagination + +=== ORDER BY → sort + +**SQL:** +[source,sql] +---- +SELECT name, age FROM person ORDER BY age DESC, name ASC; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $name, has age $age; +sort $age desc, $name asc; +select $name, $age; +---- + +=== LIMIT and OFFSET + +**SQL:** +[source,sql] +---- +SELECT name FROM person ORDER BY name LIMIT 10 OFFSET 20; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=0] +match + $p isa person, has name $name; +sort $name; +offset 20; +limit 10; +select $name; +---- + +== Advanced Operations + +=== DISTINCT → distinct + +**SQL:** +[source,sql] +---- +SELECT DISTINCT age FROM person; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has age $age; +select $age; +distinct; +---- + +=== Subqueries → Functions or Nested match + +**SQL:** +[source,sql] +---- +SELECT name FROM person +WHERE age > (SELECT AVG(age) FROM person); +---- + +**TypeQL using functions:** +[,typeql] +---- +#!test[read, count=1] +with fun avg_age() -> integer: + match $p isa person, has age $age; + return mean($age); +match + $p isa person, has name $name, has age $age; + $age > avg_age(); +select $name; +---- + +=== UNION → or patterns + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE age < 25 +UNION +SELECT name FROM person WHERE age > 35; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=1] +match + $p isa person, has name $name, has age $age; + { $age < 25; } or { $age > 35; }; +select $name; +---- + +=== EXISTS → not { not { ... } } + +**SQL:** +[source,sql] +---- +SELECT name FROM person p +WHERE EXISTS ( + SELECT 1 FROM employment e + WHERE e.employee_id = p.id OR e.employer_id = p.id +); +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=2] +match + $p isa person, has name $name; + not { not { $e isa employment, links (employee: $p); }; }; +select $name; +---- + +_This will be improved in future versions of TypeQL!_ + +=== IN → Multiple value constraints + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE age IN (25, 30, 35); +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=1] +match + $p isa person, has name $name, has age $age; + { $age == 25; } or { $age == 30; } or { $age == 35; }; +select $name; +---- + +=== LIKE → like (regex) + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE email LIKE '%@example.com'; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $name, has email $email; + $email like ".*@example.com"; +select $name; +---- + +=== NULL checks → not { ... } + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE email IS NULL; +---- + +**TypeQL:** + +TypeQL doesn't have NULLs. A value is either existing or absent in the database: + +[,typeql] +---- +#!test[read, count=0] +match + $p isa person, has name $name; + not { $p has email; }; +select $name; +---- + +**SQL:** +[source,sql] +---- +SELECT name FROM person WHERE email IS NOT NULL; +---- + +**TypeQL:** +[,typeql] +---- +#!test[read, count=3] +match + $p isa person, has name $name, has email $email; +select $name; +---- + +Note that for optional matches like when using LEFT/RIGHT joins, TypeQL's `try` construct can create `None` values. Unlike `null`s in SQL, a `None` is not a value that can be stored. It's also only possible to generated for explicitly known optional variables (ie. ones generated in `try` patterns.) + +Fundamentally, in SQL a `null` is an extra value that any column could contain. In TypeQL, a `None` is a marker given to a variable when it is unset, but it is never used as a value in the data. In other words, any typical value in SQL has the range of possible values of: ` + null`. In TypeQL, variables that are explicitly optional can hold `Either` instead of just `Value`. + +== Key Differences and Concepts + +=== No Explicit FROM Clause + +In TypeQL, there's no `FROM` clause. The `match` pattern implicitly defines what data to search through by specifying types and their relationships. + +=== Pattern-Based Matching + +TypeQL uses pattern matching rather than table joins. Relations are first-class citizens that naturally connect entities, making complex queries more intuitive. + +=== Strong Typing + +TypeQL is strongly typed. Variables are bound to specific types (entities, relations, attributes), and the query engine enforces type safety. Optional variables are known at query compilation time and validated throughout. + +=== Pipeline Model + +TypeQL queries are pipelines where each clause processes a stream of answers. This allows for powerful composition: + +[,typeql] +---- +#!test[read, count=1] +match + $p isa person, has name $name, has age $age; +reduce $avg_age = mean($age); +match $avg_age > 30; +---- + +=== Relations vs Foreign Keys + +In SQL, relationships are represented through foreign keys. In TypeQL, relationships are explicit relations that can have their own attributes and connect multiple entities. + +**SQL (with foreign keys):** +[source,sql] +---- +CREATE TABLE employment ( + id INT PRIMARY KEY, + employee_id INT REFERENCES person(id), + employer_id INT REFERENCES company(id), + start_date DATE +); +---- + +**TypeQL:** +[,typeql] +---- +define + relation employment, relates employee, relates employer, owns start-date; + attribute start-date, value datetime; + person plays employment:employee; + company plays employment:employer; +---- + +== Summary Table + +[cols="^1,^1,^1", options="header"] +|=== +| SQL Construct | TypeQL Equivalent | Notes + +| `SELECT` | `match` + `fetch`/`select` | Pattern matching replaces table selection +| `FROM` | Implicit in `match` patterns | Types are specified in the pattern +| `WHERE` | Constraints in `match` | Filtering is part of the pattern +| `JOIN` | Relation patterns | Relations naturally connect entities +| `INNER JOIN` | Required relation patterns | Both sides must exist +| `LEFT JOIN` | Optional patterns with `try` | Use `try` for optional matches +| `RIGHT JOIN` | Optional patterns with `try` | Reverse pattern direction +| `INSERT` | `insert` | Similar syntax, pattern-based +| `UPDATE` | `update` | Only for single-value attributes +| `DELETE` | `delete` | Pattern-based deletion +| `GROUP BY` | `reduce ... groupby` | Aggregation with grouping +| `HAVING` | `reduce` + `match` | Filter aggregated results +| `ORDER BY` | `sort` | Similar functionality +| `LIMIT` | `limit` | Identical +| `OFFSET` | `offset` | Identical +| `DISTINCT` | `distinct` | Identical +| `COUNT/SUM/AVG/etc` | `reduce` with functions | `count`, `sum`, `mean`, etc. +| `UNION` | `or` patterns | Pattern disjunction +| `EXISTS` | `not { not { ... } }` | Double negation pattern +| `IN` | `or` with multiple `==` | Pattern disjunction +| `LIKE` | `like` with regex | Uses regex syntax +| `IS NULL` | `not { ... has ... }` | Negation pattern +| `IS NOT NULL` | Implicit in pattern | If matched, it exists +|=== + +== References + +* xref:{page-version}@core-concepts::typeql/query-clauses.adoc[Query Clauses & Pipelining] +* xref:{page-version}@core-concepts::typeql/query-variables-patterns.adoc[Query Variables and Patterns] +* xref:{page-version}@typeql-reference::pipelines/index.adoc[TypeQL Pipeline Reference] +* xref:{page-version}@typeql-reference::patterns/index.adoc[TypeQL Pattern Reference] + diff --git a/core-concepts/modules/ROOT/partials/nav.adoc b/core-concepts/modules/ROOT/partials/nav.adoc index b020c2791..a0374cb3b 100644 --- a/core-concepts/modules/ROOT/partials/nav.adoc +++ b/core-concepts/modules/ROOT/partials/nav.adoc @@ -23,6 +23,8 @@ ** xref:{page-version}@core-concepts::typeql/invalid-patterns.adoc[] // ** xref:{page-version}@core-concepts::typeql/best-practices.adoc[] ** xref:{page-version}@core-concepts::typeql/glossary.adoc[] +** xref:{page-version}@core-concepts::typeql/sql-vs-typeql.adoc[] + * xref:{page-version}@core-concepts::drivers/index.adoc[] ** xref:{page-version}@core-concepts::drivers/overview.adoc[] From 4a24609a94169f3e70a09f3351f8f1409d601257 Mon Sep 17 00:00:00 2001 From: joshua Date: Fri, 14 Nov 2025 15:04:48 -0500 Subject: [PATCH 2/4] Polish sql-vs-typeql --- .../ROOT/pages/typeql/sql-vs-typeql.adoc | 179 +++++++++++------- 1 file changed, 110 insertions(+), 69 deletions(-) diff --git a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc index 957c03c9d..49982faa5 100644 --- a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc +++ b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc @@ -1,31 +1,33 @@ -= SQL versus TypeQL guide += Understanding SQL vs. TypeQL :test-typeql: linear This guide helps SQL users understand TypeQL by mapping familiar SQL constructs to their TypeQL equivalents. -TypeQL is a declarative query language for TypeDB that uses pattern matching rather than table joins. [NOTE] ==== -TypeQL operates on a graph data model (entities, relations, and attributes) rather than tables and rows. -This fundamental difference means that some SQL concepts map directly, while others require a different approach. +TypeQL operates on a data model using entity, relation, and attribute types (and their instances), instead of tables and rows. +This difference means that some SQL concepts map directly, while others require a different approach. ==== -== Schema Setup +== Schema setup -For the examples in this guide, we'll use a simple schema: +For the examples here, we can use a simple TypeQL schema: [,typeql] ---- #!test[schema] define - entity person, owns name, owns email, owns age; attribute name, value string; attribute email, value string; attribute age, value integer; relation employment, relates employee, relates employer; - person plays employment:employee; + entity person, owns name, owns email, owns age, plays employment:employee; entity company, owns name, plays employment:employer; - company owns name; +---- + +With the following data: +[,typeql] +---- #!test[write, commit] insert $alice isa person, has name "Alice", has email "alice@example.com", has age 30; @@ -34,23 +36,24 @@ insert $acme isa company, has name "Acme Corp"; $e1 isa employment, links (employee: $alice, employer: $acme); ---- + ==== -== Basic Query Operations +== Data retrieval === SELECT → match + fetch/select In SQL, `SELECT` retrieves data from tables. In TypeQL, `match` finds data by pattern, and `fetch` or `select` projects the results. **SQL:** -[source,sql] +[sql] ---- SELECT name, email FROM person; ---- **TypeQL:** -In TypeQL, use SELECT to filter out unwanted data: +In TypeQL, use `select` to filter out unwanted data. This is a filtering operation, and does not set ordering of result variables. [,typeql] ---- @@ -60,25 +63,25 @@ match select $name, $email; # eliminates $p ---- -Special case: if you're returning data and want to format it through a `fetch` clause, it can also work as a projection: +If you want your data to be returned in JSON, you can format it through a `fetch` clause, which also works as a projection: [,typeql] ---- #!test[read, count=3] match - $p isa person, has name $name, has email $email; + $p isa person, has name $name; fetch { "name": $name, - "email": $email + "email": $p.email # projection in the fetch clause }; ---- -=== WHERE → Pattern Constraints +=== WHERE → match SQL's `WHERE` clause filters rows. In TypeQL, constraints are part of the `match` pattern. **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE age > 25; ---- @@ -103,12 +106,14 @@ select $name; ---- -==== AND/OR Conditions in WHERE clauses +==== AND/OR conditions in WHERE clauses -SQL uses `AND` and `OR` in `WHERE` clauses. TypeQL uses pattern conjunction (comma or semicolon) and disjunction (`or`). +SQL uses `AND` (conjunction) and `OR` (disjunction) in `WHERE` clauses. -**SQL:** -[source,sql] +In TypeQL, statements and composite patterns are combined an implicit `and` by default: `;` and `,` both are implicit conjunctions. An `or` pattern is used to explicitly represent a disjunction. + +**SQL with AND:** +[sql] ---- SELECT name FROM person WHERE age > 25 AND email LIKE '%@example.com'; ---- @@ -125,7 +130,7 @@ select $name; ---- **SQL with OR:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE age < 20 OR age > 30; ---- @@ -140,7 +145,7 @@ match select $name; ---- -=== JOIN → Relation Patterns +=== JOIN → relation patterns SQL uses `JOIN` to combine data from multiple tables. In TypeQL, relations naturally connect entities through patterns. The type of JOIN determines which entities are included in the results when relationships don't exist. @@ -150,7 +155,7 @@ The type of JOIN determines which entities are included in the results when rela An INNER JOIN returns only rows where there is a match in both tables. In TypeQL, this is the default behavior when matching relations. **SQL:** -[source,sql] +[sql] ---- SELECT p.name, e.id FROM person p @@ -167,7 +172,7 @@ match select $pname, $e; ---- -This returns only persons who have an employment relation. +This returns only persons who have an employment relation attached. ==== LEFT JOIN @@ -175,7 +180,7 @@ A LEFT JOIN returns all rows from the left table (person), and matching rows fro If there's no match, NULL values are returned for the right table columns. In TypeQL, use the `try` construct for optional matching. **SQL:** -[source,sql] +[sql] ---- SELECT p.name, e.id FROM person p @@ -203,7 +208,7 @@ A RIGHT JOIN returns all rows from the right table (employment), and matching ro If there's no match, NULL values are returned for the left table columns. In TypeQL, reverse the pattern to start from the right side. **SQL:** -[source,sql] +[sql] ---- SELECT p.name, e.id FROM person p @@ -232,7 +237,7 @@ A CROSS JOIN returns the Cartesian product of two tables - every row from the fi In TypeQL, match entities from both types independently without requiring any relation between them. **SQL:** -[source,sql] +[sql] ---- SELECT p.name, e.id FROM person p @@ -249,21 +254,20 @@ match select $pname, $e; ---- -This returns every combination of person and employment, regardless of whether they have any relationship. -The result set size is the product of the number of persons and employments. +This returns every combination of person and employment, regardless of whether they have any relationship. The result set size is the product of the number of persons and employments. ==== FULL OUTER JOIN A FULL OUTER JOIN returns all rows from both tables, with NULL values where there's no match. -TypeQL doesn't yet have an efficient equivalent of full outer joins! Please leave us a message if this operation is required for your application. +Full outer joins do not translate well to TypeQL. If your application requires an equivalent of a full outer join, please contact us! -== Data Modification +== Data modification === INSERT → insert **SQL:** -[source,sql] +[sql] ---- INSERT INTO person (name, email, age) VALUES ('David', 'david@example.com', 28); ---- @@ -277,7 +281,7 @@ insert ---- **SQL INSERT with SELECT:** -[source,sql] +[sql] ---- INSERT INTO employment (employee_id, employer_id) SELECT p.id, c.id @@ -299,7 +303,7 @@ insert === UPDATE → update **SQL:** -[source,sql] +[sql] ---- UPDATE person SET email = 'newemail@example.com' WHERE name = 'Alice'; ---- @@ -322,7 +326,7 @@ It automatically deletes the old value and inserts the new one. === DELETE → delete **SQL:** -[source,sql] +[sql] ---- DELETE FROM person WHERE name = 'Charlie'; ---- @@ -337,7 +341,7 @@ delete $p; ---- **SQL DELETE with JOIN:** -[source,sql] +[sql] ---- DELETE e FROM employment e INNER JOIN person p ON e.employee_id = p.id @@ -354,12 +358,12 @@ match delete $e; ---- -== Aggregation and Grouping +== Aggregation and grouping === GROUP BY → reduce with groupby **SQL:** -[source,sql] +[sql] ---- SELECT age, COUNT(*) as count FROM person @@ -378,7 +382,7 @@ reduce $count = count groupby $age; === Aggregate Functions **SQL:** -[source,sql] +[sql] ---- SELECT COUNT(*) as total, @@ -406,7 +410,7 @@ reduce === HAVING → reduce + match **SQL:** -[source,sql] +[sql] ---- SELECT age, COUNT(*) as count FROM person @@ -424,12 +428,12 @@ reduce $count = count groupby $age; match $count > 1; ---- -== Sorting and Pagination +== Sorting and pagination === ORDER BY → sort **SQL:** -[source,sql] +[sql] ---- SELECT name, age FROM person ORDER BY age DESC, name ASC; ---- @@ -447,12 +451,15 @@ select $name, $age; === LIMIT and OFFSET **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person ORDER BY name LIMIT 10 OFFSET 20; ---- **TypeQL:** + +In TypeQL, we call stages like `limit`/`offset` (also, `select`, `sort`) _stream operators_, since they only manipulate the answer stream and don't access the database. Order of stages matter! + [,typeql] ---- #!test[read, count=0] @@ -464,12 +471,16 @@ limit 10; select $name; ---- -== Advanced Operations +Here we take the `match` answers, `sort` them, skip the first 20 with `offset`, the take the first `10`. + +If we did `limit`, then `order`, we would `limit` the sorted answers to the first then, and then try to skip the first 20 with `offset` - resulting in 0 answers every time! + +== Advanced operations === DISTINCT → distinct **SQL:** -[source,sql] +[sql] ---- SELECT DISTINCT age FROM person; ---- @@ -484,16 +495,16 @@ select $age; distinct; ---- -=== Subqueries → Functions or Nested match +=== Subqueries → functions **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE age > (SELECT AVG(age) FROM person); ---- -**TypeQL using functions:** +**TypeQL:** [,typeql] ---- #!test[read, count=1] @@ -509,7 +520,7 @@ select $name; === UNION → or patterns **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE age < 25 UNION @@ -517,6 +528,9 @@ SELECT name FROM person WHERE age > 35; ---- **TypeQL:** + +In TypeQL, you have multiple ways to execute this union using `or`: + [,typeql] ---- #!test[read, count=1] @@ -526,10 +540,24 @@ match select $name; ---- +[,typeql] +---- +#!test[read, count=1] +match + { + $p isa person, has name $name, has age < 25; + } or { + $p isa person, has name $name, has age > 35; + }; +select $name; +---- + +Both are valid! + === EXISTS → not { not { ... } } **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person p WHERE EXISTS ( @@ -548,12 +576,14 @@ match select $name; ---- +This reads as: we're looking for a person such that it's not true that: there is not an employment involving that person. + _This will be improved in future versions of TypeQL!_ -=== IN → Multiple value constraints +=== IN → multiple value constraints **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE age IN (25, 30, 35); ---- @@ -568,10 +598,20 @@ match select $name; ---- +TypeQL's roadmap features lists, which would allow writing the following: +[,typeql] +---- +# NOTE: roadmap feature! +match + $p isa person, has name $name, has age $age; + $age in [25, 30, 35]; +select $name; +---- + === LIKE → like (regex) **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE email LIKE '%@example.com'; ---- @@ -589,14 +629,14 @@ select $name; === NULL checks → not { ... } **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE email IS NULL; ---- **TypeQL:** -TypeQL doesn't have NULLs. A value is either existing or absent in the database: +TypeQL doesn't store NULLs. A value is either existing or absent in the database: [,typeql] ---- @@ -608,7 +648,7 @@ select $name; ---- **SQL:** -[source,sql] +[sql] ---- SELECT name FROM person WHERE email IS NOT NULL; ---- @@ -618,29 +658,29 @@ SELECT name FROM person WHERE email IS NOT NULL; ---- #!test[read, count=3] match - $p isa person, has name $name, has email $email; + $p isa person, has name $name, has email $_; select $name; ---- -Note that for optional matches like when using LEFT/RIGHT joins, TypeQL's `try` construct can create `None` values. Unlike `null`s in SQL, a `None` is not a value that can be stored. It's also only possible to generated for explicitly known optional variables (ie. ones generated in `try` patterns.) +Note that for optional matches like when using LEFT/RIGHT joins, TypeQL's `try` construct can create `None` values. Unlike `null`s in SQL, a `None` is not a value that can be stored. It's also only possible to generated for optional variables (ie. ones generated in `try` patterns.) -Fundamentally, in SQL a `null` is an extra value that any column could contain. In TypeQL, a `None` is a marker given to a variable when it is unset, but it is never used as a value in the data. In other words, any typical value in SQL has the range of possible values of: ` + null`. In TypeQL, variables that are explicitly optional can hold `Either` instead of just `Value`. +Fundamentally, in SQL a `null` is an extra value that any column could contain. In TypeQL, a `None` is a marker given to a variable when it is unset, but it is never used _as_ a value in the data. In other words, any typical value in SQL has the range of possible values of: ` + null`. In TypeQL, variables that are optional can hold `Either` instead of the typical `Value`. -== Key Differences and Concepts +== Key differences and concepts -=== No Explicit FROM Clause +=== No explicit FROM clause In TypeQL, there's no `FROM` clause. The `match` pattern implicitly defines what data to search through by specifying types and their relationships. -=== Pattern-Based Matching +=== Pattern-based matching TypeQL uses pattern matching rather than table joins. Relations are first-class citizens that naturally connect entities, making complex queries more intuitive. -=== Strong Typing +=== Strong typing TypeQL is strongly typed. Variables are bound to specific types (entities, relations, attributes), and the query engine enforces type safety. Optional variables are known at query compilation time and validated throughout. -=== Pipeline Model +=== Pipeline model TypeQL queries are pipelines where each clause processes a stream of answers. This allows for powerful composition: @@ -653,12 +693,12 @@ reduce $avg_age = mean($age); match $avg_age > 30; ---- -=== Relations vs Foreign Keys +=== Relations vs foreign keys In SQL, relationships are represented through foreign keys. In TypeQL, relationships are explicit relations that can have their own attributes and connect multiple entities. **SQL (with foreign keys):** -[source,sql] +[sql] ---- CREATE TABLE employment ( id INT PRIMARY KEY, @@ -678,7 +718,7 @@ define company plays employment:employer; ---- -== Summary Table +== Summary table [cols="^1,^1,^1", options="header"] |=== @@ -691,6 +731,7 @@ define | `INNER JOIN` | Required relation patterns | Both sides must exist | `LEFT JOIN` | Optional patterns with `try` | Use `try` for optional matches | `RIGHT JOIN` | Optional patterns with `try` | Reverse pattern direction +| `CROSS JOIN` | Disconnected match statements | | `INSERT` | `insert` | Similar syntax, pattern-based | `UPDATE` | `update` | Only for single-value attributes | `DELETE` | `delete` | Pattern-based deletion @@ -700,7 +741,7 @@ define | `LIMIT` | `limit` | Identical | `OFFSET` | `offset` | Identical | `DISTINCT` | `distinct` | Identical -| `COUNT/SUM/AVG/etc` | `reduce` with functions | `count`, `sum`, `mean`, etc. +| `COUNT`/`SUM`/`AVG`/`etc. | `reduce` with `count`/`sum`/`mean`/etc. | Similar, assigned to a variable| | `UNION` | `or` patterns | Pattern disjunction | `EXISTS` | `not { not { ... } }` | Double negation pattern | `IN` | `or` with multiple `==` | Pattern disjunction From c679ae7008150fbd42882fabb146fe6a0267a2d7 Mon Sep 17 00:00:00 2001 From: joshua Date: Fri, 14 Nov 2025 16:50:09 -0500 Subject: [PATCH 3/4] Fix tests --- .../examples/driver/python_driver_setup.py | 2 +- .../examples/driver/python_driver_usage.py | 2 +- .../ROOT/pages/typeql/query-clauses.adoc | 2 +- .../ROOT/pages/typeql/sql-vs-typeql.adoc | 41 +++++++++--------- dependencies/typedb/artifacts.bzl | 2 +- test/runners/typeql_runner.py | 43 ++++++------------- .../ROOT/pages/statements/relates.adoc | 1 - 7 files changed, 39 insertions(+), 54 deletions(-) diff --git a/core-concepts/modules/ROOT/examples/driver/python_driver_setup.py b/core-concepts/modules/ROOT/examples/driver/python_driver_setup.py index ee30529c0..f061de4ee 100644 --- a/core-concepts/modules/ROOT/examples/driver/python_driver_setup.py +++ b/core-concepts/modules/ROOT/examples/driver/python_driver_setup.py @@ -14,7 +14,7 @@ credentials = Credentials("admin", "password") #end::constants_credentials[] #tag::constants_options[] -options = DriverOptions(is_tls_enabled=True, tls_root_ca_path=None) +options = DriverOptions(is_tls_enabled=False, tls_root_ca_path=None) #end::constants_options[] #end::constants[] #end::import_and_constants[] diff --git a/core-concepts/modules/ROOT/examples/driver/python_driver_usage.py b/core-concepts/modules/ROOT/examples/driver/python_driver_usage.py index 7cde66bb2..462326a45 100644 --- a/core-concepts/modules/ROOT/examples/driver/python_driver_usage.py +++ b/core-concepts/modules/ROOT/examples/driver/python_driver_usage.py @@ -6,7 +6,7 @@ DB_NAME = "my_database" address = "localhost:1729" credentials = Credentials("admin", "password") -options = DriverOptions(is_tls_enabled=True, tls_root_ca_path=None) +options = DriverOptions(is_tls_enabled=False, tls_root_ca_path=None) #end::import_and_constants[] #tag::driver_create[] diff --git a/core-concepts/modules/ROOT/pages/typeql/query-clauses.adoc b/core-concepts/modules/ROOT/pages/typeql/query-clauses.adoc index 05e613ac3..0babc802b 100644 --- a/core-concepts/modules/ROOT/pages/typeql/query-clauses.adoc +++ b/core-concepts/modules/ROOT/pages/typeql/query-clauses.adoc @@ -150,7 +150,7 @@ Removes duplicate answers in the stream. [,typeql] ---- -#!test[read, count=3] +#!test[read, count=4] match { $p isa person; } or { $p has name "John"; }; distinct; # Make sure that the person with name John is returned at most once. diff --git a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc index 49982faa5..eb74a220d 100644 --- a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc +++ b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc @@ -67,7 +67,7 @@ If you want your data to be returned in JSON, you can format it through a `fetch [,typeql] ---- -#!test[read, count=3] +#!test[read, count=3, documents] match $p isa person, has name $name; fetch { @@ -228,8 +228,8 @@ match select $pname; ---- -This returns all employments, with employee names if they exist. -Employments without matching persons will still appear in the results (with `$pname` unbound). +This returns employments' employee names if they exist. +Employments without matching persons will still appear in the results (with `$pname` unbound, set to `None`). ==== CROSS JOIN @@ -351,7 +351,7 @@ WHERE p.name = 'Alice'; **TypeQL:** [,typeql] ---- -#!test[write, rollback, count=1] +#!test[write, rollback, count=2] match $p isa person, has name "Alice"; $e isa employment, links (employee: $p); @@ -373,7 +373,7 @@ GROUP BY age; **TypeQL:** [,typeql] ---- -#!test[read, count=3] +#!test[read, count=4] match $p isa person, has age $age; reduce $count = count groupby $age; @@ -441,7 +441,7 @@ SELECT name, age FROM person ORDER BY age DESC, name ASC; **TypeQL:** [,typeql] ---- -#!test[read, count=3] +#!test[read, count=4] match $p isa person, has name $name, has age $age; sort $age desc, $name asc; @@ -488,7 +488,7 @@ SELECT DISTINCT age FROM person; **TypeQL:** [,typeql] ---- -#!test[read, count=3] +#!test[read, count=4] match $p isa person, has age $age; select $age; @@ -507,14 +507,15 @@ WHERE age > (SELECT AVG(age) FROM person); **TypeQL:** [,typeql] ---- -#!test[read, count=1] -with fun avg_age() -> integer: +#!test[read, count=2] +with fun avg_age() -> double: match $p isa person, has age $age; return mean($age); match $p isa person, has name $name, has age $age; - $age > avg_age(); -select $name; + let $avg = avg_age(); + $age > $avg; +select $avg, $name, $age; ---- === UNION → or patterns @@ -524,7 +525,7 @@ select $name; ---- SELECT name FROM person WHERE age < 25 UNION -SELECT name FROM person WHERE age > 35; +SELECT name FROM person WHERE age >= 35; ---- **TypeQL:** @@ -536,7 +537,7 @@ In TypeQL, you have multiple ways to execute this union using `or`: #!test[read, count=1] match $p isa person, has name $name, has age $age; - { $age < 25; } or { $age > 35; }; + { $age < 25; } or { $age >= 35; }; select $name; ---- @@ -547,7 +548,7 @@ match { $p isa person, has name $name, has age < 25; } or { - $p isa person, has name $name, has age > 35; + $p isa person, has name $name, has age >= 35; }; select $name; ---- @@ -569,7 +570,7 @@ WHERE EXISTS ( **TypeQL:** [,typeql] ---- -#!test[read, count=2] +#!test[read, count=1] match $p isa person, has name $name; not { not { $e isa employment, links (employee: $p); }; }; @@ -591,7 +592,7 @@ SELECT name FROM person WHERE age IN (25, 30, 35); **TypeQL:** [,typeql] ---- -#!test[read, count=1] +#!test[read, count=3] match $p isa person, has name $name, has age $age; { $age == 25; } or { $age == 30; } or { $age == 35; }; @@ -619,7 +620,7 @@ SELECT name FROM person WHERE email LIKE '%@example.com'; **TypeQL:** [,typeql] ---- -#!test[read, count=3] +#!test[read, count=4] match $p isa person, has name $name, has email $email; $email like ".*@example.com"; @@ -643,7 +644,7 @@ TypeQL doesn't store NULLs. A value is either existing or absent in the database #!test[read, count=0] match $p isa person, has name $name; - not { $p has email; }; + not { $p has email $_; }; select $name; ---- @@ -656,7 +657,7 @@ SELECT name FROM person WHERE email IS NOT NULL; **TypeQL:** [,typeql] ---- -#!test[read, count=3] +#!test[read, count=4] match $p isa person, has name $name, has email $_; select $name; @@ -690,7 +691,7 @@ TypeQL queries are pipelines where each clause processes a stream of answers. Th match $p isa person, has name $name, has age $age; reduce $avg_age = mean($age); -match $avg_age > 30; +match $avg_age > 29; ---- === Relations vs foreign keys diff --git a/dependencies/typedb/artifacts.bzl b/dependencies/typedb/artifacts.bzl index 4ad09d0a8..faf62efe6 100644 --- a/dependencies/typedb/artifacts.bzl +++ b/dependencies/typedb/artifacts.bzl @@ -25,5 +25,5 @@ def typedb_artifact(): artifact_name = "typedb-all-{platform}-{version}.{ext}", tag_source = deployment["artifact"]["release"]["download"], commit_source = deployment["artifact"]["snapshot"]["download"], - tag = "3.4.4" + commit = "828fecc98675712b15e4f6a9b0b2e7ae54d19766" ) diff --git a/test/runners/typeql_runner.py b/test/runners/typeql_runner.py index 0118d310d..e29e030af 100644 --- a/test/runners/typeql_runner.py +++ b/test/runners/typeql_runner.py @@ -93,39 +93,28 @@ def run_failing_queries(self, queries: List[str], type: TransactionType) -> str: return FailureMode.Commit return FailureMode.NoFailure - def run_transaction(self, queries: List[str], type: TransactionType, counted=False, rollback=False, documents=False) -> Union[int, None]: - count_var_name = "automatic_test_count" - if counted: - queries[-1] = queries[-1] + f"\nreduce ${count_var_name} = count;" + def run_transaction(self, queries: List[str], type: TransactionType, rollback=False) -> Union[int, None]: with self.driver.transaction(self.db, type) as tx: try: - promises = [] - results = [] for q in queries: - promises.append(tx.query(q)) - for p in promises: - results.append(p.resolve()) + results = tx.query(q).resolve() + if results.is_ok(): + results = [] + elif results.is_concept_rows(): + results = list(results.as_concept_rows()) + elif results.is_concept_documents(): + results = list(results.as_concept_documents()) + else: + print(f"Unknown result type: {results}") if rollback: tx.rollback() tx.close() elif type == TransactionType.READ: - for r in results: - if documents: - consumed_iterator = list(r.as_concept_documents()) - else: - consumed_iterator = list(r.as_concept_rows()) tx.close() else: tx.commit() - if counted: - if documents: - raise Exception("Counting currently relies on Reduce, which is not compatible with Documents") - if type == TransactionType.READ: - count = consumed_iterator[0].get(count_var_name).get_integer() - else: - last_result = list(results[-1].as_concept_rows()) - count = last_result[0].get(count_var_name).get_integer() - return count + # the count only returned for the count of the last query + return len(results) except Exception as e: raise Exception(f"{e}") from e @@ -162,10 +151,6 @@ def run_test(self, parsed_test: ParsedTest, adoc_path: str): reference_count = int(parsed_test.config[TEST_COUNT_KEY]) counted = True - documents = False - if parsed_test.config.get(TEST_DOCUMENTS_KEY) is not None: - documents = True - if parsed_test.config.get(TEST_FAIL_KEY): ref_failure_mode = FailureMode.NoFailure match parsed_test.config[TEST_FAIL_KEY]: @@ -177,11 +162,11 @@ def run_test(self, parsed_test: ParsedTest, adoc_path: str): if failure_mode != ref_failure_mode: raise RuntimeError(f"[{adoc_path}]: Failure mode: expected {ref_failure_mode} but got {failure_mode}") elif counted == True: - count = self.run_transaction(parsed_test.segments, type, counted, rollback, documents) + count = self.run_transaction(parsed_test.segments, type, rollback) if count != reference_count: raise RuntimeError(f"[{adoc_path}]: Query count: expected {reference_count} but got {count}") else: - self.run_transaction(parsed_test.segments, type, counted, rollback, documents) + self.run_transaction(parsed_test.segments, type, rollback) self.after_run_test(parsed_test) diff --git a/typeql-reference/modules/ROOT/pages/statements/relates.adoc b/typeql-reference/modules/ROOT/pages/statements/relates.adoc index 36ab78bb4..a5ef0ae26 100644 --- a/typeql-reference/modules/ROOT/pages/statements/relates.adoc +++ b/typeql-reference/modules/ROOT/pages/statements/relates.adoc @@ -100,6 +100,5 @@ The previous query can return results for different role types with the same ` Date: Fri, 14 Nov 2025 16:51:30 -0500 Subject: [PATCH 4/4] Cleanup unused 'documents' flags in typeql tested snippets --- core-concepts/modules/ROOT/pages/typedb/crud.adoc | 2 +- core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc | 2 +- home/modules/ROOT/pages/get-started/query-composition.adoc | 2 +- test/runners/typeql_runner.py | 1 - typeql-reference/modules/ROOT/pages/pipelines/fetch.adoc | 2 +- typeql-reference/modules/ROOT/pages/pipelines/insert.adoc | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/core-concepts/modules/ROOT/pages/typedb/crud.adoc b/core-concepts/modules/ROOT/pages/typedb/crud.adoc index 5bb3f354c..6911276bf 100644 --- a/core-concepts/modules/ROOT/pages/typedb/crud.adoc +++ b/core-concepts/modules/ROOT/pages/typedb/crud.adoc @@ -176,7 +176,7 @@ This allows structuring the output of the query to map precisely to the structur [,typeql] ---- -#!test[read, documents] +#!test[read] match $p isa person; fetch { diff --git a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc index eb74a220d..bf5c5c98c 100644 --- a/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc +++ b/core-concepts/modules/ROOT/pages/typeql/sql-vs-typeql.adoc @@ -67,7 +67,7 @@ If you want your data to be returned in JSON, you can format it through a `fetch [,typeql] ---- -#!test[read, count=3, documents] +#!test[read, count=3] match $p isa person, has name $name; fetch { diff --git a/home/modules/ROOT/pages/get-started/query-composition.adoc b/home/modules/ROOT/pages/get-started/query-composition.adoc index c1f011c38..f9d17584a 100644 --- a/home/modules/ROOT/pages/get-started/query-composition.adoc +++ b/home/modules/ROOT/pages/get-started/query-composition.adoc @@ -720,7 +720,7 @@ fetch { //// [,typeql] ---- -#!test[read, documents] +#!test[read] include::./query-composition.adoc[tag=fetch1] ---- diff --git a/test/runners/typeql_runner.py b/test/runners/typeql_runner.py index e29e030af..9b372a085 100644 --- a/test/runners/typeql_runner.py +++ b/test/runners/typeql_runner.py @@ -24,7 +24,6 @@ TEST_TXN_READ_KEY = "read" TEST_ROLLBACK_KEY = "rollback" TEST_COUNT_KEY = "count" -TEST_DOCUMENTS_KEY = "documents" TEST_JUMP_KEY = "jump" TEST_FAIL_KEY = "fail_at" TEST_FAIL_COMMIT_VAL = "commit" diff --git a/typeql-reference/modules/ROOT/pages/pipelines/fetch.adoc b/typeql-reference/modules/ROOT/pages/pipelines/fetch.adoc index d22f71a06..dc26314b1 100644 --- a/typeql-reference/modules/ROOT/pages/pipelines/fetch.adoc +++ b/typeql-reference/modules/ROOT/pages/pipelines/fetch.adoc @@ -216,7 +216,7 @@ include::{page-version}@reference::example$tql/schema_statements_functions.tql[t [,typeql] ---- -#!test[read, documents] +#!test[read] match $group isa group; fetch { diff --git a/typeql-reference/modules/ROOT/pages/pipelines/insert.adoc b/typeql-reference/modules/ROOT/pages/pipelines/insert.adoc index 244e4cf09..3aac116d0 100644 --- a/typeql-reference/modules/ROOT/pages/pipelines/insert.adoc +++ b/typeql-reference/modules/ROOT/pages/pipelines/insert.adoc @@ -61,7 +61,7 @@ include::{page-version}@reference::example$tql/schema_statements_functions.tql[t [,typeql] ---- -#!test[write, documents] +#!test[write] insert $group isa group, has group-id "";