From 69f30e46a6a398197968cbfe1355659d1c5171d7 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sat, 24 Oct 2026 02:33:46 +0900 Subject: [PATCH 1/2] docs: add 'Which package for which mode' and 'Coming from Postgres or SQLite' pages Co-Authored-By: Claude Opus 5.5 --- docs/antora.yml | 8 + docs/modules/ROOT/nav.adoc | 2 + .../pages/coming-from-postgres-or-sqlite.adoc | 289 ++++++++++++++++++ docs/modules/ROOT/pages/packages.adoc | 78 +++++ 4 files changed, 377 insertions(+) create mode 100644 docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc create mode 100644 docs/modules/ROOT/pages/packages.adoc diff --git a/docs/antora.yml b/docs/antora.yml index c362d5f..8d3d4af 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -13,5 +13,13 @@ asciidoc: import-stmt: import arcadedb_embedded as arcadedb native-pkg-name: arcadedb-embedded-native native-import-name: arcadedb_native + # Other names a reader meets on PyPI (the pages that compare packages use these). + short-pkg-name: arcadedb + short-native-pkg-name: arcadedb-native + jvm-pkg-name: arcadedb-jvm + driver-pkg-name: arcadedb-driver + grpc-driver-pkg-name: arcadedb-driver-grpc + community-driver-pkg-name: arcadedb-python + drivers-repo-url: https://github.com/ArcadeData/arcadedb-drivers repo-url: https://github.com/ArcadeData/arcadedb-python pypi-url: https://pypi.org/project/arcadedb-embedded/ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 28708e8..40970e0 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,5 +1,7 @@ * xref:index.adoc[ArcadeDB Python] * xref:installation.adoc[Installation] +* xref:packages.adoc[Which package for which mode] * xref:quickstart.adoc[Quickstart] +* xref:coming-from-postgres-or-sqlite.adoc[Coming from Postgres or SQLite] * How-to ** xref:how-to/databases-and-queries.adoc[Databases and queries] diff --git a/docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc b/docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc new file mode 100644 index 0000000..3b4ea76 --- /dev/null +++ b/docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc @@ -0,0 +1,289 @@ += Coming from Postgres or SQLite +:description: ArcadeDB SQL has no JOIN. See common join patterns rewritten with links, edges, MATCH, TRAVERSE and subqueries, with openCypher equivalents. + +ArcadeDB's SQL is its own dialect. It is document-oriented and it has no `JOIN`: records point at other records by record ID, and a query follows those pointers. If you know Postgres or SQLite, most of `SELECT`, `WHERE`, `GROUP BY`, `ORDER BY` and `LIMIT` will look familiar. Relationships are where you change habits. + +* A foreign key becomes a *link property*, a column that holds the record ID of another record. You read through it with a dot: `customer.name`. +* A join table, or a relationship you walk in both directions, becomes an *edge*. An edge is a record of its own, so it can carry columns such as a quantity. +* A recursive CTE becomes `TRAVERSE` or `MATCH` with a depth. +* openCypher works on the same data, and for relationships it is often the shortest way to say it. + +Every script on this page ran against the published `{pkg-name}` 26.10.2.post1 wheel, and every output is what it printed. Rows come back as dictionaries, and the order of an unsorted list is the order the engine returns it. + +== Set up the data + +The schema below is the usual customers, orders and products. `Order.customer` is a link property, and the `Placed` and `Contains` edges are the same relationships stored as edges. A real application picks one form per relationship; this page uses both so that each pattern can show its form. + +[source,python,subs="attributes+"] +---- +{import-stmt} + + +def rows(language, query): + for record in db.query(language, query): + print(record.to_dict()) + + +with arcadedb.create_database("./shop") as db: + db.command("sql", "CREATE VERTEX TYPE Customer") + db.command("sql", "CREATE VERTEX TYPE Product") + db.command("sql", "CREATE VERTEX TYPE Order") + db.command("sql", "CREATE PROPERTY Order.customer LINK OF Customer") + db.command("sql", "CREATE EDGE TYPE Placed") + db.command("sql", "CREATE EDGE TYPE Contains") + db.command("sql", "CREATE VERTEX TYPE Person") + db.command("sql", "CREATE EDGE TYPE Follows") + + with db.transaction(): + for name in ("Alice", "Bob", "Carol"): + db.command("sql", "INSERT INTO Customer SET name = ?", name) + for name, price in (("Widget", 5.0), ("Gadget", 12.5)): + db.command("sql", "INSERT INTO Product SET name = ?, price = ?", name, price) + for oid, cust, total in ((1, "Alice", 17.5), (2, "Alice", 5.0), (3, "Bob", 12.5)): + db.command("sql", "INSERT INTO Order SET id = ?, total = ?, " + "customer = (SELECT FROM Customer WHERE name = ?)", oid, total, cust) + db.command("sql", "CREATE EDGE Placed FROM (SELECT FROM Customer WHERE name = ?) " + "TO (SELECT FROM Order WHERE id = ?)", cust, oid) + for oid, prod, qty in ((1, "Widget", 1), (1, "Gadget", 1), (2, "Widget", 1), (3, "Gadget", 1)): + db.command("sql", "CREATE EDGE Contains FROM (SELECT FROM Order WHERE id = ?) " + "TO (SELECT FROM Product WHERE name = ?) SET qty = ?", oid, prod, qty) + for n in ("Ann", "Ben", "Cy", "Di"): + db.command("sql", "INSERT INTO Person SET name = ?", n) + for a, b in (("Ann", "Ben"), ("Ben", "Cy"), ("Cy", "Di")): + db.command("sql", "CREATE EDGE Follows FROM (SELECT FROM Person WHERE name = ?) " + "TO (SELECT FROM Person WHERE name = ?)", a, b) + + # ... the patterns below run here, inside the `with` block +---- + +NOTE: The `rows()` helper is only for printing. The query strings are the part to copy. + +== Pattern 1: many-to-one, a foreign key + +In Postgres: + +[source,sql] +---- +SELECT o.id, o.total, c.name AS customer +FROM orders o JOIN customers c ON c.id = o.customer_id +ORDER BY o.id; +---- + +In ArcadeDB the order holds a link to its customer, and you read through the link: + +[source,python] +---- +rows("sql", "SELECT id, total, customer.name AS customer FROM Order ORDER BY id") +---- + +[source,text] +---- +{'id': 1, 'total': 17.5, 'customer': 'Alice'} +{'id': 2, 'total': 5.0, 'customer': 'Alice'} +{'id': 3, 'total': 12.5, 'customer': 'Bob'} +---- + +There is no join to plan and no `customer_id` to keep in step with a key: the link is the relationship. Inserting an order takes the customer as a subquery, as the setup shows: `customer = (SELECT FROM Customer WHERE name = ?)`. openCypher reads relationships as edges, so for the same answer there use the `Placed` edge, as in pattern 2. + +== Pattern 2: one-to-many, the other way round + +In Postgres: + +[source,sql] +---- +SELECT c.name, o.id AS order_id +FROM customers c JOIN orders o ON o.customer_id = c.id +ORDER BY o.id; +---- + +ArcadeDB SQL has two forms. `out('Placed')` follows the edge from each customer to its orders; `MATCH` writes the pattern: + +[source,python] +---- +rows("sql", "SELECT name, out('Placed').id AS order_ids FROM Customer ORDER BY name") +rows("sql", "MATCH {type: Customer, as: c}-Placed->{type: Order, as: o} " + "RETURN c.name AS customer, o.id AS order_id ORDER BY order_id") +rows("opencypher", "MATCH (c:Customer)-[:Placed]->(o:Order) " + "RETURN c.name AS customer, o.id AS order_id ORDER BY order_id") +---- + +[source,text] +---- +{'name': 'Alice', 'order_ids': [2, 1]} +{'name': 'Bob', 'order_ids': [3]} +{'name': 'Carol', 'order_ids': []} +{'customer': 'Alice', 'order_id': 1} +{'customer': 'Alice', 'order_id': 2} +{'customer': 'Bob', 'order_id': 3} +{'customer': 'Alice', 'order_id': 1} +{'customer': 'Alice', 'order_id': 2} +{'customer': 'Bob', 'order_id': 3} +---- + +The first query returns one row per customer with a list, so Carol, who has no orders, still appears. That is closer to a `LEFT JOIN` with `array_agg` than to the join above. The `MATCH` forms return one row per pair, like the join. + +== Pattern 3: a join table becomes an edge with properties + +In Postgres `order_items` joins orders to products and holds the quantity: + +[source,sql] +---- +SELECT oi.order_id, p.name AS product, oi.qty +FROM order_items oi JOIN products p ON p.id = oi.product_id +ORDER BY oi.order_id, p.name; +---- + +In ArcadeDB the `Contains` edge is that row. It points from the order to the product, and `qty` is a property of the edge. Query the edge type directly, and use `outV()` and `inV()` for its two ends: + +[source,python] +---- +rows("sql", "SELECT outV().id AS order_id, inV().name AS product, qty FROM Contains " + "ORDER BY order_id, product") +rows("opencypher", "MATCH (o:Order)-[r:Contains]->(p:Product) " + "RETURN o.id AS order_id, p.name AS product, r.qty AS qty ORDER BY order_id, product") +---- + +[source,text] +---- +{'order_id': 1, 'product': 'Gadget', 'qty': 1} +{'order_id': 1, 'product': 'Widget', 'qty': 1} +{'order_id': 2, 'product': 'Widget', 'qty': 1} +{'order_id': 3, 'product': 'Gadget', 'qty': 1} +{'order_id': 1, 'product': 'Gadget', 'qty': 1} +{'order_id': 1, 'product': 'Widget', 'qty': 1} +{'order_id': 2, 'product': 'Widget', 'qty': 1} +{'order_id': 3, 'product': 'Gadget', 'qty': 1} +---- + +NOTE: `out.id` does not work here: in the edge record `out` is the raw link, and the call `outV()` is what reads the vertex behind it. The SQL query returned `None` for both columns until it used `outV()` and `inV()`. + +== Pattern 4: rows with no match, the anti-join + +In Postgres: + +[source,sql] +---- +SELECT c.name FROM customers c +LEFT JOIN orders o ON o.customer_id = c.id +WHERE o.id IS NULL; +---- + +[source,python] +---- +rows("sql", "SELECT name FROM Customer WHERE out('Placed').size() = 0") +rows("opencypher", "MATCH (c:Customer) WHERE NOT (c)-[:Placed]->() RETURN c.name AS name") +---- + +[source,text] +---- +{'name': 'Carol'} +{'name': 'Carol'} +---- + +== Pattern 5: aggregate across a join + +In Postgres: + +[source,sql] +---- +SELECT c.name AS customer, sum(o.total) AS spent +FROM customers c JOIN orders o ON o.customer_id = c.id +GROUP BY c.name ORDER BY c.name; +---- + +Group by the linked value. In SQL, `customer.name` reads through the link property; in openCypher you match the edge: + +[source,python] +---- +rows("sql", "SELECT customer.name AS customer, sum(total) AS spent FROM Order " + "GROUP BY customer.name ORDER BY customer") +rows("opencypher", "MATCH (c:Customer)-[:Placed]->(o:Order) " + "RETURN c.name AS customer, sum(o.total) AS spent ORDER BY customer") +---- + +[source,text] +---- +{'customer': 'Alice', 'spent': 22.5} +{'customer': 'Bob', 'spent': 12.5} +{'customer': 'Alice', 'spent': 22.5} +{'customer': 'Bob', 'spent': 12.5} +---- + +A subquery covers the semi-join, the `WHERE id IN (SELECT ...)` form. This one finds the customers who ordered a Widget: + +[source,python] +---- +rows("sql", "SELECT name FROM Customer WHERE @rid IN " + "(SELECT customer FROM Order WHERE out('Contains').name CONTAINS 'Widget')") +---- + +[source,text] +---- +{'name': 'Alice'} +---- + +== Pattern 6: a self-join of any depth + +In Postgres, "who does Ann follow, directly or through one more person" is a self-join or a recursive CTE: + +[source,sql] +---- +WITH RECURSIVE reach(id, depth) AS ( + SELECT followee_id, 1 FROM follows WHERE follower_id = (SELECT id FROM people WHERE name = 'Ann') + UNION ALL + SELECT f.followee_id, r.depth + 1 FROM follows f JOIN reach r ON f.follower_id = r.id WHERE r.depth < 2 +) +SELECT p.name FROM reach r JOIN people p ON p.id = r.id; +---- + +In ArcadeDB the depth is a parameter of the walk: + +[source,python] +---- +rows("sql", "SELECT name FROM (TRAVERSE out('Follows') FROM " + "(SELECT FROM Person WHERE name = 'Ann') MAXDEPTH 2) WHERE $depth > 0") +rows("sql", "MATCH {type: Person, as: a, where: (name = 'Ann')}" + ".out('Follows'){as: b, while: ($depth < 2), where: ($depth > 0)} RETURN b.name AS name") +rows("opencypher", "MATCH (a:Person {name: 'Ann'})-[:Follows*1..2]->(b:Person) " + "RETURN b.name AS name ORDER BY name") +---- + +[source,text] +---- +{'name': 'Ben'} +{'name': 'Cy'} +{'name': 'Ben'} +{'name': 'Cy'} +{'name': 'Ben'} +{'name': 'Cy'} +---- + +`TRAVERSE` includes the start record at depth 0, which is why the first query filters on `$depth > 0`. + +== What a JOIN does here + +[source,python] +---- +db.query("sql", "SELECT c.name, o.id FROM Customer c JOIN Order o ON o.customer = c.@rid") +---- + +[source,text] +---- +ArcadeDBError Query failed: com.arcadedb.exception.CommandSQLParsingException: SQL syntax error at line 1, column 36: mismatched input 'JOIN' expecting {, ';'} +SELECT c.name, o.id FROM Customer c JOIN Order o ON o.customer = c.@rid + ^ +---- + +== What does not translate + +* *`JOIN` in any form.* The keyword is a syntax error. Rewrite each join as one of the patterns above. +* *ORMs such as SQLAlchemy.* They generate `JOIN` queries, which this dialect does not parse. Use `db.query` and `db.command` with the statements on this page. +* *BI tools and anything else that writes joins for you.* Tools that build `JOIN ... ON` queries from a schema, over a driver or a wire protocol, generate SQL that fails the same way, because the dialect has no `JOIN` to emit. Write the queries by hand, as above. +* *A foreign key as a declared constraint.* A link property holds a record ID and is followed on read. This page does not cover whether the database checks it on write. + +If your queries are mostly relationships, start with openCypher: it is the shorter language for them, and the patterns above show each one. + +== See also + +* xref:how-to/databases-and-queries.adoc[Databases and queries] +* xref:packages.adoc[Which package for which mode] diff --git a/docs/modules/ROOT/pages/packages.adoc b/docs/modules/ROOT/pages/packages.adoc new file mode 100644 index 0000000..185cb9f --- /dev/null +++ b/docs/modules/ROOT/pages/packages.adoc @@ -0,0 +1,78 @@ += Which package for which mode +:description: Pick between the embedded JVM package, the embedded native package, the official server drivers and the community driver of the same name. + +Several PyPI packages mention ArcadeDB, and two of them look alike. This page says which is which and which one to install for your process. + +== The short answer + +* One Python process owns the database and you want the simplest setup: install `{pkg-name}`. It runs the engine on the JVM, inside your process, and bundles its own Java runtime. +* A short-lived process, or one where memory and start-up time matter: install `{native-pkg-name}`. It runs the engine as a GraalVM native image. It is a preview. +* Several processes, or several services, share one database: run an ArcadeDB server and talk to it with `{driver-pkg-name}` (HTTP) or `{grpc-driver-pkg-name}` (gRPC). + +== Rule of thumb + +[cols="2,3"] +|=== +| Your process | Use + +| Short-lived and footprint-sensitive: a CLI, a function that runs per request, a test run, a notebook cell that opens a database and exits +| The native build, `{native-pkg-name}`. It starts faster and holds less memory than the JVM build. + +| Long-lived, with heavy repeated scans or big loads +| The JVM build, `{pkg-name}`. The JVM warms up and then runs long scans and bulk loads faster than the native build. + +| Several processes or services read and write the same database +| The server, with a driver. An embedded database is owned by one process. +|=== + +The two builds differ in speed in both directions: which one is ahead depends on whether you measure the first pass after a start or the warm passes. Measure with your own data before you commit to one. The two embedded packages expose the same API, so changing from one to the other is a change of package name and import name. + +== The packages + +[cols="2,2,3"] +|=== +| Package | Kind | What it is + +| `{pkg-name}` +| Embedded, JVM build +| The ArcadeDB engine inside your Python process, through https://jpype.readthedocs.io/[JPype]. The wheel carries a Java runtime. Import it as `{import-name}`. This is the package this documentation covers. + +| `{native-pkg-name}` +| Embedded, GraalVM native build (preview) +| The same API, with the engine compiled ahead of time: no JVM and no JPype at run time, fast start and a small footprint. Import it as `{native-import-name}`. + +| `{short-pkg-name}` +| Embedded, JVM build +| The name this package moves to in the middle of November. Until then it is a placeholder on PyPI that installs `{pkg-name}`. + +| `{short-native-pkg-name}` +| Embedded, native build (preview) +| The name the native package moves to at the same time. Until then it is an alias that installs `{native-pkg-name}`. + +| `{jvm-pkg-name}` +| Embedded, JVM build +| A reserved name for the JVM build. Until the move it installs `{pkg-name}`, the same as `{short-pkg-name}`. + +| `{driver-pkg-name}` +| Server client, HTTP +| The official Python HTTP client for a running ArcadeDB server, generated from the server's OpenAPI contract. It is part of {drivers-repo-url}[ArcadeData/arcadedb-drivers]. It does not contain the engine. + +| `{grpc-driver-pkg-name}` +| Server client, gRPC +| The official Python gRPC client for the server's data plane, generated from its protobuf contract. Same repository as above. It does not contain the engine. + +| `{community-driver-pkg-name}` +| Server client, HTTP (community) +| A community HTTP driver by Steve Reiner and others, at https://github.com/stevereiner/arcadedb-python[stevereiner/arcadedb-python]. It is *not* related to this repository, whose name is also `arcadedb-python`, and it does not contain the engine. Check which one a tutorial installs before you copy its code. +|=== + +The names `{pkg-name}` and `{native-pkg-name}` are the current ones. After the move in the middle of November the embedded packages are `{short-pkg-name}` and `{short-native-pkg-name}`. Check this page again after the move. + +== Embedded or server + +Embedded means the database is a directory that your process opens. There is no network hop and no server to run. It fits one application with one database. When a second process needs the same data, switch to the server: you run ArcadeDB itself and connect with `{driver-pkg-name}` or `{grpc-driver-pkg-name}`. The query languages, SQL and openCypher, are the same in both. You can also start the server from inside an embedded process with `create_server()` to open the Studio UI; the xref:installation.adoc[installation page] says what that adds to the wheel. + +== See also + +* xref:installation.adoc[Installation] +* xref:coming-from-postgres-or-sqlite.adoc[Coming from Postgres or SQLite] From 36cb9e0be5922efece2e5bfce13486d4b60ee33b Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sat, 24 Oct 2026 02:42:10 +0900 Subject: [PATCH 2/2] docs: drop the Postgres/SQLite page (the main docs cover it), link instead; trim the packages page and the how-to to Python specifics Co-Authored-By: Claude Opus 5.5 --- docs/antora.yml | 2 +- docs/modules/ROOT/nav.adoc | 1 - .../pages/coming-from-postgres-or-sqlite.adoc | 289 ------------------ .../pages/how-to/databases-and-queries.adoc | 10 +- docs/modules/ROOT/pages/index.adoc | 1 + docs/modules/ROOT/pages/packages.adoc | 44 +-- docs/modules/ROOT/pages/quickstart.adoc | 2 +- 7 files changed, 24 insertions(+), 325 deletions(-) delete mode 100644 docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc diff --git a/docs/antora.yml b/docs/antora.yml index 8d3d4af..c8ad805 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -20,6 +20,6 @@ asciidoc: driver-pkg-name: arcadedb-driver grpc-driver-pkg-name: arcadedb-driver-grpc community-driver-pkg-name: arcadedb-python - drivers-repo-url: https://github.com/ArcadeData/arcadedb-drivers + community-repo-url: https://github.com/stevereiner/arcadedb-python repo-url: https://github.com/ArcadeData/arcadedb-python pypi-url: https://pypi.org/project/arcadedb-embedded/ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 40970e0..39980fc 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -2,6 +2,5 @@ * xref:installation.adoc[Installation] * xref:packages.adoc[Which package for which mode] * xref:quickstart.adoc[Quickstart] -* xref:coming-from-postgres-or-sqlite.adoc[Coming from Postgres or SQLite] * How-to ** xref:how-to/databases-and-queries.adoc[Databases and queries] diff --git a/docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc b/docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc deleted file mode 100644 index 3b4ea76..0000000 --- a/docs/modules/ROOT/pages/coming-from-postgres-or-sqlite.adoc +++ /dev/null @@ -1,289 +0,0 @@ -= Coming from Postgres or SQLite -:description: ArcadeDB SQL has no JOIN. See common join patterns rewritten with links, edges, MATCH, TRAVERSE and subqueries, with openCypher equivalents. - -ArcadeDB's SQL is its own dialect. It is document-oriented and it has no `JOIN`: records point at other records by record ID, and a query follows those pointers. If you know Postgres or SQLite, most of `SELECT`, `WHERE`, `GROUP BY`, `ORDER BY` and `LIMIT` will look familiar. Relationships are where you change habits. - -* A foreign key becomes a *link property*, a column that holds the record ID of another record. You read through it with a dot: `customer.name`. -* A join table, or a relationship you walk in both directions, becomes an *edge*. An edge is a record of its own, so it can carry columns such as a quantity. -* A recursive CTE becomes `TRAVERSE` or `MATCH` with a depth. -* openCypher works on the same data, and for relationships it is often the shortest way to say it. - -Every script on this page ran against the published `{pkg-name}` 26.10.2.post1 wheel, and every output is what it printed. Rows come back as dictionaries, and the order of an unsorted list is the order the engine returns it. - -== Set up the data - -The schema below is the usual customers, orders and products. `Order.customer` is a link property, and the `Placed` and `Contains` edges are the same relationships stored as edges. A real application picks one form per relationship; this page uses both so that each pattern can show its form. - -[source,python,subs="attributes+"] ----- -{import-stmt} - - -def rows(language, query): - for record in db.query(language, query): - print(record.to_dict()) - - -with arcadedb.create_database("./shop") as db: - db.command("sql", "CREATE VERTEX TYPE Customer") - db.command("sql", "CREATE VERTEX TYPE Product") - db.command("sql", "CREATE VERTEX TYPE Order") - db.command("sql", "CREATE PROPERTY Order.customer LINK OF Customer") - db.command("sql", "CREATE EDGE TYPE Placed") - db.command("sql", "CREATE EDGE TYPE Contains") - db.command("sql", "CREATE VERTEX TYPE Person") - db.command("sql", "CREATE EDGE TYPE Follows") - - with db.transaction(): - for name in ("Alice", "Bob", "Carol"): - db.command("sql", "INSERT INTO Customer SET name = ?", name) - for name, price in (("Widget", 5.0), ("Gadget", 12.5)): - db.command("sql", "INSERT INTO Product SET name = ?, price = ?", name, price) - for oid, cust, total in ((1, "Alice", 17.5), (2, "Alice", 5.0), (3, "Bob", 12.5)): - db.command("sql", "INSERT INTO Order SET id = ?, total = ?, " - "customer = (SELECT FROM Customer WHERE name = ?)", oid, total, cust) - db.command("sql", "CREATE EDGE Placed FROM (SELECT FROM Customer WHERE name = ?) " - "TO (SELECT FROM Order WHERE id = ?)", cust, oid) - for oid, prod, qty in ((1, "Widget", 1), (1, "Gadget", 1), (2, "Widget", 1), (3, "Gadget", 1)): - db.command("sql", "CREATE EDGE Contains FROM (SELECT FROM Order WHERE id = ?) " - "TO (SELECT FROM Product WHERE name = ?) SET qty = ?", oid, prod, qty) - for n in ("Ann", "Ben", "Cy", "Di"): - db.command("sql", "INSERT INTO Person SET name = ?", n) - for a, b in (("Ann", "Ben"), ("Ben", "Cy"), ("Cy", "Di")): - db.command("sql", "CREATE EDGE Follows FROM (SELECT FROM Person WHERE name = ?) " - "TO (SELECT FROM Person WHERE name = ?)", a, b) - - # ... the patterns below run here, inside the `with` block ----- - -NOTE: The `rows()` helper is only for printing. The query strings are the part to copy. - -== Pattern 1: many-to-one, a foreign key - -In Postgres: - -[source,sql] ----- -SELECT o.id, o.total, c.name AS customer -FROM orders o JOIN customers c ON c.id = o.customer_id -ORDER BY o.id; ----- - -In ArcadeDB the order holds a link to its customer, and you read through the link: - -[source,python] ----- -rows("sql", "SELECT id, total, customer.name AS customer FROM Order ORDER BY id") ----- - -[source,text] ----- -{'id': 1, 'total': 17.5, 'customer': 'Alice'} -{'id': 2, 'total': 5.0, 'customer': 'Alice'} -{'id': 3, 'total': 12.5, 'customer': 'Bob'} ----- - -There is no join to plan and no `customer_id` to keep in step with a key: the link is the relationship. Inserting an order takes the customer as a subquery, as the setup shows: `customer = (SELECT FROM Customer WHERE name = ?)`. openCypher reads relationships as edges, so for the same answer there use the `Placed` edge, as in pattern 2. - -== Pattern 2: one-to-many, the other way round - -In Postgres: - -[source,sql] ----- -SELECT c.name, o.id AS order_id -FROM customers c JOIN orders o ON o.customer_id = c.id -ORDER BY o.id; ----- - -ArcadeDB SQL has two forms. `out('Placed')` follows the edge from each customer to its orders; `MATCH` writes the pattern: - -[source,python] ----- -rows("sql", "SELECT name, out('Placed').id AS order_ids FROM Customer ORDER BY name") -rows("sql", "MATCH {type: Customer, as: c}-Placed->{type: Order, as: o} " - "RETURN c.name AS customer, o.id AS order_id ORDER BY order_id") -rows("opencypher", "MATCH (c:Customer)-[:Placed]->(o:Order) " - "RETURN c.name AS customer, o.id AS order_id ORDER BY order_id") ----- - -[source,text] ----- -{'name': 'Alice', 'order_ids': [2, 1]} -{'name': 'Bob', 'order_ids': [3]} -{'name': 'Carol', 'order_ids': []} -{'customer': 'Alice', 'order_id': 1} -{'customer': 'Alice', 'order_id': 2} -{'customer': 'Bob', 'order_id': 3} -{'customer': 'Alice', 'order_id': 1} -{'customer': 'Alice', 'order_id': 2} -{'customer': 'Bob', 'order_id': 3} ----- - -The first query returns one row per customer with a list, so Carol, who has no orders, still appears. That is closer to a `LEFT JOIN` with `array_agg` than to the join above. The `MATCH` forms return one row per pair, like the join. - -== Pattern 3: a join table becomes an edge with properties - -In Postgres `order_items` joins orders to products and holds the quantity: - -[source,sql] ----- -SELECT oi.order_id, p.name AS product, oi.qty -FROM order_items oi JOIN products p ON p.id = oi.product_id -ORDER BY oi.order_id, p.name; ----- - -In ArcadeDB the `Contains` edge is that row. It points from the order to the product, and `qty` is a property of the edge. Query the edge type directly, and use `outV()` and `inV()` for its two ends: - -[source,python] ----- -rows("sql", "SELECT outV().id AS order_id, inV().name AS product, qty FROM Contains " - "ORDER BY order_id, product") -rows("opencypher", "MATCH (o:Order)-[r:Contains]->(p:Product) " - "RETURN o.id AS order_id, p.name AS product, r.qty AS qty ORDER BY order_id, product") ----- - -[source,text] ----- -{'order_id': 1, 'product': 'Gadget', 'qty': 1} -{'order_id': 1, 'product': 'Widget', 'qty': 1} -{'order_id': 2, 'product': 'Widget', 'qty': 1} -{'order_id': 3, 'product': 'Gadget', 'qty': 1} -{'order_id': 1, 'product': 'Gadget', 'qty': 1} -{'order_id': 1, 'product': 'Widget', 'qty': 1} -{'order_id': 2, 'product': 'Widget', 'qty': 1} -{'order_id': 3, 'product': 'Gadget', 'qty': 1} ----- - -NOTE: `out.id` does not work here: in the edge record `out` is the raw link, and the call `outV()` is what reads the vertex behind it. The SQL query returned `None` for both columns until it used `outV()` and `inV()`. - -== Pattern 4: rows with no match, the anti-join - -In Postgres: - -[source,sql] ----- -SELECT c.name FROM customers c -LEFT JOIN orders o ON o.customer_id = c.id -WHERE o.id IS NULL; ----- - -[source,python] ----- -rows("sql", "SELECT name FROM Customer WHERE out('Placed').size() = 0") -rows("opencypher", "MATCH (c:Customer) WHERE NOT (c)-[:Placed]->() RETURN c.name AS name") ----- - -[source,text] ----- -{'name': 'Carol'} -{'name': 'Carol'} ----- - -== Pattern 5: aggregate across a join - -In Postgres: - -[source,sql] ----- -SELECT c.name AS customer, sum(o.total) AS spent -FROM customers c JOIN orders o ON o.customer_id = c.id -GROUP BY c.name ORDER BY c.name; ----- - -Group by the linked value. In SQL, `customer.name` reads through the link property; in openCypher you match the edge: - -[source,python] ----- -rows("sql", "SELECT customer.name AS customer, sum(total) AS spent FROM Order " - "GROUP BY customer.name ORDER BY customer") -rows("opencypher", "MATCH (c:Customer)-[:Placed]->(o:Order) " - "RETURN c.name AS customer, sum(o.total) AS spent ORDER BY customer") ----- - -[source,text] ----- -{'customer': 'Alice', 'spent': 22.5} -{'customer': 'Bob', 'spent': 12.5} -{'customer': 'Alice', 'spent': 22.5} -{'customer': 'Bob', 'spent': 12.5} ----- - -A subquery covers the semi-join, the `WHERE id IN (SELECT ...)` form. This one finds the customers who ordered a Widget: - -[source,python] ----- -rows("sql", "SELECT name FROM Customer WHERE @rid IN " - "(SELECT customer FROM Order WHERE out('Contains').name CONTAINS 'Widget')") ----- - -[source,text] ----- -{'name': 'Alice'} ----- - -== Pattern 6: a self-join of any depth - -In Postgres, "who does Ann follow, directly or through one more person" is a self-join or a recursive CTE: - -[source,sql] ----- -WITH RECURSIVE reach(id, depth) AS ( - SELECT followee_id, 1 FROM follows WHERE follower_id = (SELECT id FROM people WHERE name = 'Ann') - UNION ALL - SELECT f.followee_id, r.depth + 1 FROM follows f JOIN reach r ON f.follower_id = r.id WHERE r.depth < 2 -) -SELECT p.name FROM reach r JOIN people p ON p.id = r.id; ----- - -In ArcadeDB the depth is a parameter of the walk: - -[source,python] ----- -rows("sql", "SELECT name FROM (TRAVERSE out('Follows') FROM " - "(SELECT FROM Person WHERE name = 'Ann') MAXDEPTH 2) WHERE $depth > 0") -rows("sql", "MATCH {type: Person, as: a, where: (name = 'Ann')}" - ".out('Follows'){as: b, while: ($depth < 2), where: ($depth > 0)} RETURN b.name AS name") -rows("opencypher", "MATCH (a:Person {name: 'Ann'})-[:Follows*1..2]->(b:Person) " - "RETURN b.name AS name ORDER BY name") ----- - -[source,text] ----- -{'name': 'Ben'} -{'name': 'Cy'} -{'name': 'Ben'} -{'name': 'Cy'} -{'name': 'Ben'} -{'name': 'Cy'} ----- - -`TRAVERSE` includes the start record at depth 0, which is why the first query filters on `$depth > 0`. - -== What a JOIN does here - -[source,python] ----- -db.query("sql", "SELECT c.name, o.id FROM Customer c JOIN Order o ON o.customer = c.@rid") ----- - -[source,text] ----- -ArcadeDBError Query failed: com.arcadedb.exception.CommandSQLParsingException: SQL syntax error at line 1, column 36: mismatched input 'JOIN' expecting {, ';'} -SELECT c.name, o.id FROM Customer c JOIN Order o ON o.customer = c.@rid - ^ ----- - -== What does not translate - -* *`JOIN` in any form.* The keyword is a syntax error. Rewrite each join as one of the patterns above. -* *ORMs such as SQLAlchemy.* They generate `JOIN` queries, which this dialect does not parse. Use `db.query` and `db.command` with the statements on this page. -* *BI tools and anything else that writes joins for you.* Tools that build `JOIN ... ON` queries from a schema, over a driver or a wire protocol, generate SQL that fails the same way, because the dialect has no `JOIN` to emit. Write the queries by hand, as above. -* *A foreign key as a declared constraint.* A link property holds a record ID and is followed on read. This page does not cover whether the database checks it on write. - -If your queries are mostly relationships, start with openCypher: it is the shorter language for them, and the patterns above show each one. - -== See also - -* xref:how-to/databases-and-queries.adoc[Databases and queries] -* xref:packages.adoc[Which package for which mode] diff --git a/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc b/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc index b064c1c..6e47e77 100644 --- a/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc +++ b/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc @@ -204,7 +204,7 @@ total: 2 === Parameters -Bind values as parameters instead of pasting them into the statement text. A pasted value can change the statement (SQL injection), and a quote in a name breaks it. ArcadeDB also caches parsed statements by their text, so every distinct pasted value is parsed again. Identifiers such as type and property names cannot be bound and belong in the text. +Bind values as parameters instead of pasting them into the statement text. Identifiers such as type and property names cannot be bound and belong in the text. * Positional: one `?` per value, from the extra arguments (`db.query("sql", q, "Alice", 25)`) or from one list or tuple (`db.query("sql", q, ["Alice", 25])`). * Named: `:name` in the statement and a dict as the one extra argument. A named parameter can hold a collection, as in `age IN :ages`. @@ -219,15 +219,17 @@ Bind values as parameters instead of pasting them into the statement text. A pas === Pagination -For a large type, page by `@rid` instead of `SKIP`. Each page is `WHERE @rid > ? LIMIT ?`, and the cursor is the `@rid` of the last row of the previous page. Start with `"#-1:-1"`. The script above shows the loop with a page size of one so that two short pages print. +Page a large type by `@rid` instead of `SKIP`: each page is `WHERE @rid > ? LIMIT ?`, and the cursor is the `@rid` of the last row of the previous page. Start with `"#-1:-1"`. The script above runs the loop with a page size of one so that two short pages print. === SQLScript -`db.command("sqlscript", script)` runs several statements in one call. Without an explicit `RETURN`, the result set holds the last executed statement, DDL such as `CREATE` and `ALTER` included. That is why the script above reports the `ALTER TYPE` statement. +`db.command("sqlscript", script)` runs several statements in one call, and the result set holds the last executed statement, which is why the script above reports the `ALTER TYPE` statement. The script syntax is in the xref:arcadedb::reference/sql/sql-script.adoc[SQLScript reference]. === openCypher -The same two calls run openCypher: pass `"opencypher"` as the language. The script creates an edge with a `CREATE` pattern and reads it back with `MATCH`. +The same two calls run openCypher: pass `"opencypher"` as the language. The script creates an edge with a `CREATE` pattern and reads it back with `MATCH`. The language is documented in the xref:arcadedb::reference/cypher/chapter.adoc[openCypher reference]. + +NOTE: ArcadeDB SQL has no JOINs, so SQLAlchemy and other ORMs, and tools that generate `JOIN` queries, do not work with these two calls. Write the statements yourself, following links as in xref:arcadedb::reference/sql/sql-introduction.adoc#no-joins[No JOINs], or use openCypher for relationships. == Next steps diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 21b9871..15631ff 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -57,6 +57,7 @@ Choose the embedded bindings for a single script, a notebook, or an application The two meet in one case. The bindings bundle an optional HTTP server with the Studio web UI. It is off until you call `create_server()`. The process that owns the database keeps direct access, and other processes connect over HTTP. That server lives and dies with your Python process. For a database that must outlive any one client, or for HA, replication and TLS, run the regular xref:arcadedb::index.adoc[ArcadeDB server] instead. +[[jvm-vs-native]] == JVM build or native build Two packages ship for now. Both give you the same engine and the same API. diff --git a/docs/modules/ROOT/pages/packages.adoc b/docs/modules/ROOT/pages/packages.adoc index 185cb9f..cc96422 100644 --- a/docs/modules/ROOT/pages/packages.adoc +++ b/docs/modules/ROOT/pages/packages.adoc @@ -7,7 +7,7 @@ Several PyPI packages mention ArcadeDB, and two of them look alike. This page sa * One Python process owns the database and you want the simplest setup: install `{pkg-name}`. It runs the engine on the JVM, inside your process, and bundles its own Java runtime. * A short-lived process, or one where memory and start-up time matter: install `{native-pkg-name}`. It runs the engine as a GraalVM native image. It is a preview. -* Several processes, or several services, share one database: run an ArcadeDB server and talk to it with `{driver-pkg-name}` (HTTP) or `{grpc-driver-pkg-name}` (gRPC). +* Several processes, or several services, share one database: run an ArcadeDB server and talk to it with a driver (see <>). == Rule of thumb @@ -22,12 +22,12 @@ Several PyPI packages mention ArcadeDB, and two of them look alike. This page sa | The JVM build, `{pkg-name}`. The JVM warms up and then runs long scans and bulk loads faster than the native build. | Several processes or services read and write the same database -| The server, with a driver. An embedded database is owned by one process. +| The server, with a driver. An embedded database is owned by one process; see xref:index.adoc#embedded-vs-http[Embedded bindings or the HTTP driver]. |=== -The two builds differ in speed in both directions: which one is ahead depends on whether you measure the first pass after a start or the warm passes. Measure with your own data before you commit to one. The two embedded packages expose the same API, so changing from one to the other is a change of package name and import name. +Which build is ahead depends on whether you measure the first pass after a start or the warm passes, so measure with your own data before you commit to one. The two embedded packages expose the same API; xref:index.adoc#jvm-vs-native[JVM build or native build] compares them. -== The packages +== The embedded packages [cols="2,2,3"] |=== @@ -35,44 +35,30 @@ The two builds differ in speed in both directions: which one is ahead depends on | `{pkg-name}` | Embedded, JVM build -| The ArcadeDB engine inside your Python process, through https://jpype.readthedocs.io/[JPype]. The wheel carries a Java runtime. Import it as `{import-name}`. This is the package this documentation covers. +| The engine inside your Python process, through https://jpype.readthedocs.io/[JPype], with a bundled Java runtime. Import it as `{import-name}`. This is the package this component documents. | `{native-pkg-name}` | Embedded, GraalVM native build (preview) -| The same API, with the engine compiled ahead of time: no JVM and no JPype at run time, fast start and a small footprint. Import it as `{native-import-name}`. +| The same API with the engine compiled ahead of time: no JVM and no JPype. Import it as `{native-import-name}`. | `{short-pkg-name}` | Embedded, JVM build -| The name this package moves to in the middle of November. Until then it is a placeholder on PyPI that installs `{pkg-name}`. +| The name `{pkg-name}` moves to in the middle of November. Until then a placeholder on PyPI that installs `{pkg-name}`. | `{short-native-pkg-name}` | Embedded, native build (preview) -| The name the native package moves to at the same time. Until then it is an alias that installs `{native-pkg-name}`. +| The name `{native-pkg-name}` moves to at the same time. Until then an alias that installs it. | `{jvm-pkg-name}` | Embedded, JVM build -| A reserved name for the JVM build. Until the move it installs `{pkg-name}`, the same as `{short-pkg-name}`. - -| `{driver-pkg-name}` -| Server client, HTTP -| The official Python HTTP client for a running ArcadeDB server, generated from the server's OpenAPI contract. It is part of {drivers-repo-url}[ArcadeData/arcadedb-drivers]. It does not contain the engine. - -| `{grpc-driver-pkg-name}` -| Server client, gRPC -| The official Python gRPC client for the server's data plane, generated from its protobuf contract. Same repository as above. It does not contain the engine. - -| `{community-driver-pkg-name}` -| Server client, HTTP (community) -| A community HTTP driver by Steve Reiner and others, at https://github.com/stevereiner/arcadedb-python[stevereiner/arcadedb-python]. It is *not* related to this repository, whose name is also `arcadedb-python`, and it does not contain the engine. Check which one a tutorial installs before you copy its code. +| A reserved name for the JVM build. Until the move it installs `{pkg-name}`. |=== -The names `{pkg-name}` and `{native-pkg-name}` are the current ones. After the move in the middle of November the embedded packages are `{short-pkg-name}` and `{short-native-pkg-name}`. Check this page again after the move. - -== Embedded or server - -Embedded means the database is a directory that your process opens. There is no network hop and no server to run. It fits one application with one database. When a second process needs the same data, switch to the server: you run ArcadeDB itself and connect with `{driver-pkg-name}` or `{grpc-driver-pkg-name}`. The query languages, SQL and openCypher, are the same in both. You can also start the server from inside an embedded process with `create_server()` to open the Studio UI; the xref:installation.adoc[installation page] says what that adds to the wheel. +[[drivers]] +== Packages that talk to a server -== See also +None of these contains the engine. They connect to a running ArcadeDB server. -* xref:installation.adoc[Installation] -* xref:coming-from-postgres-or-sqlite.adoc[Coming from Postgres or SQLite] +* `{driver-pkg-name}`: the official HTTP client. See xref:arcadedb::how-to/connectivity/drivers/python-http.adoc[Python -- HTTP driver]. +* `{grpc-driver-pkg-name}`: the official gRPC client. See xref:arcadedb::how-to/connectivity/drivers/python-grpc.adoc[Python -- gRPC driver]. +* `{community-driver-pkg-name}`: a community HTTP driver by Steve Reiner and others ({community-repo-url}[stevereiner/arcadedb-python]). It is not related to this repository, whose name is also `arcadedb-python`. Check which one a tutorial installs before you copy its code. diff --git a/docs/modules/ROOT/pages/quickstart.adoc b/docs/modules/ROOT/pages/quickstart.adoc index 188d5ca..fe3b6a3 100644 --- a/docs/modules/ROOT/pages/quickstart.adoc +++ b/docs/modules/ROOT/pages/quickstart.adoc @@ -7,7 +7,7 @@ This page builds a small database from scratch. You need the package from xref:i == Embedded mode -The database runs in your Python process. You use SQL for the schema and for CRUD, through two calls: +The database runs in your Python process. You use SQL for the schema and for CRUD, through two calls. ArcadeDB SQL is its own dialect, and it has no JOINs: records link to each other and a query follows the links (see xref:arcadedb::reference/sql/sql-introduction.adoc#no-joins[No JOINs]). * `db.command(language, statement, *params)` runs a statement that can change data or schema. * `db.query(language, statement, *params)` runs a read.