Skip to main content

pipeline status coverage report Latest Release Python version ArangoDB version

ArangoDB AQL builder

Python AQL builder for ArangoDB, a scalable multi-model database natively supporting documents, graphs and search.

Why use this?

Instead of writing raw AQL strings:

query = f'FOR u IN users FILTER u.age >= {min_age} RETURN u'

Use a fluent, chainable API:

query = AB.for_('u').in_('users').filter(AB.ref('u.age').gte(min_age)).return_('u').to_aql()

Benefits:

  • Composable: Build complex queries by chaining methods
  • Type-safe: Automatic conversion of Python values to AQL types
  • Readable: Query structure mirrors the AQL syntax
  • Maintainable: Easier to modify and extend queries programmatically

Features

  • Fluent, chainable API for building AQL queries
  • Support for all AQL operations: FOR, FILTER, LET, COLLECT, SORT, LIMIT, RETURN
  • DML operations: INSERT, UPDATE, REPLACE, REMOVE, UPSERT
  • Graph traversals with configurable depth and direction
  • All ArangoDB built-in functions available as methods
  • Automatic Python-to-AQL type conversion
  • Bind parameter support (@param, @@collection)
  • Query formatting: compact, pretty-printed, and debug modes
  • Full type hint support with IDE autocomplete for all 200+ AQL functions

Requirements

  • Python version 3.10+

Installation

pip install aql-builder --upgrade

Getting Started

Here are simple usage examples:

>>> from aql_builder import AQLBuilder as AB
>>> 
>>> AB.for_('my').in_('mycollection').return_('my._key').to_aql()
'FOR my IN mycollection RETURN my._key'
>>> 
>>> AB.for_('u').in_('users').replace('u').in_('backup').to_aql()
'FOR u IN users REPLACE u IN backup'
>>> 
>>> AB.for_('u').in_('users')
...     .filter(
...         AB.ref('u.active').eq(True)
...         .and_(AB.ref('u.age').gte(35))
...         .and_(AB.ref('u.age').lte(37))
...     )
...     .remove('u').in_('users')
...     .to_aql()
'FOR u IN users FILTER (((u.active == true) && (u.age >= 35)) && (u.age <= 37)) REMOVE u IN users'
>>> 

Working with query parameters:

>>> from aql_builder import AQLBuilder as AB
>>> 
>>> AB.for_('user').in_('@@users').filter(AB.ref('user.age').gte('@min_age')).return_('user._key').to_aql()
'FOR user IN @@users FILTER (user.age >= @min_age) RETURN user._key'
>>> 

Working with graph:

>>> from aql_builder import AQLBuilder as AB
>>> 
>>> AB.for_('v').in_graph(
...         graph='knowsGraph',
...         direction='OUTBOUND',
...         start_vertex=AB.str('users/1'),
...         min_depth=1,
...         max_depth=3
...     )
...     .return_('v._key')
...     .to_aql()
'FOR v IN 1..3 OUTBOUND "users/1" GRAPH "knowsGraph" RETURN v._key'
>>> 

More complex examples:

>>> from aql_builder import AQLBuilder as AB
>>> 
>>> AB.for_('i').in_(AB.range(1, 1000))
...     .insert({
...         'id': AB(100000).add('i'),
...         'age': AB(18).add(AB.FLOOR(AB.RAND().times(25))),
...         'name': AB.CONCAT('test', AB.TO_STRING('i')),
...         'active': False,
...         'gender': (
...             AB.ref('i').mod(2).eq(0).then('"male"').else_('"female"')
...         )
...     }).into('users')
...     .to_aql()
'FOR i IN 1..1000 INSERT
  {"id": (100000 + i),
   "age": (18 + FLOOR((RAND() * 25))),
   "name": CONCAT(test, TO_STRING(i)),
   "active": false,
   "gender": (((i % 2) == 0) ? "male" : "female")}
  INTO users'
>>> 
>>> 
>>> AB.for_('u').in_('users')
...     .filter(AB.ref('u.active').eq(True))
...     .collect({
...         'ageGroup': AB.FLOOR(AB.ref('u.age').div(5)).times(5),
...         'gender': 'u.gender'
...     }).into('group')
...     .sort('ageGroup', 'DESC')
...     .return_({
...         'ageGroup': 'ageGroup',
...         'gender': 'gender'
...     })
...     .to_aql()
'FOR u IN users
  FILTER (u.active == true)
  COLLECT ageGroup = (FLOOR((u.age / 5)) * 5), gender = u.gender INTO group
  SORT ageGroup DESC
  RETURN {"ageGroup": ageGroup, "gender": gender}'
>>>

Query Formatting

The library supports multiple output formats for AQL queries.

Compact Format (Default)

The default format produces a single-line query with normalized whitespace:

>>> from aql_builder import AQLBuilder as AB
>>>
>>> query = (
...     AB.for_('u').in_('users')
...     .filter(AB.ref('u.age').gte(18))
...     .filter(AB.ref('u.active').eq(True))
...     .sort('u.name', 'ASC')
...     .return_('u')
... )
>>>
>>> query.to_aql()
'FOR u IN users FILTER (u.age >= 18) FILTER (u.active == true) SORT u.name ASC RETURN u'
>>>

Pretty Format

Use format='pretty' for multi-line, indented output:

>>> print(query.to_aql(format='pretty'))
FOR u IN users
    FILTER (u.age >= 18)
    FILTER (u.active == true)
    SORT u.name ASC
    RETURN u
>>>
>>> # Convenience method
>>> print(query.to_aql_pretty())
FOR u IN users
    FILTER (u.age >= 18)
    FILTER (u.active == true)
    SORT u.name ASC
    RETURN u
>>>

Custom Indentation

Customize the indentation string (default is 4 spaces):

>>> print(query.to_aql(format='pretty', indent='  '))
FOR u IN users
  FILTER (u.age >= 18)
  FILTER (u.active == true)
  SORT u.name ASC
  RETURN u
>>>
>>> # Using tabs
>>> print(query.to_aql_pretty(indent='\t'))
FOR u IN users
	FILTER (u.age >= 18)
	FILTER (u.active == true)
	SORT u.name ASC
	RETURN u
>>>

Debug Mode

Add class name annotations to understand the query structure:

>>> query.to_aql(debug=True)
'FOR u IN users FILTER (u.age >= 18) FILTER (u.active == true) SORT u.name ASC RETURN u  # [ForExpression], [FilterExpression], [FilterExpression], [SortExpression], [ReturnExpression]'
>>>
>>> # Debug with pretty format
>>> print(query.to_aql(format='pretty', debug=True))
FOR u IN users            [ForExpression]
    FILTER (u.age >= 18)  [FilterExpression]
    FILTER (u.active == true)  [FilterExpression]
    SORT u.name ASC       [SortExpression]
    RETURN u              [ReturnExpression]
>>>
>>> # Convenience method
>>> query.to_aql_debug()
'FOR u IN users FILTER (u.age >= 18) FILTER (u.active == true) SORT u.name ASC RETURN u  # [ForExpression], [FilterExpression], [FilterExpression], [SortExpression], [ReturnExpression]'
>>>

Formatting Raw AQL Strings

You can also format raw AQL strings using the static method:

>>> raw_aql = 'FOR u IN users FILTER u.age >= 18 RETURN u'
>>>
>>> # Pretty format
>>> print(AB.format_aql(raw_aql, format_type='pretty'))
FOR u IN users
    FILTER u.age >= 18
    RETURN u
>>>
>>> # Compact format (normalizes whitespace)
>>> AB.format_aql(raw_aql, format_type='compact')
'FOR u IN users FILTER u.age >= 18 RETURN u'
>>>

Type Hints and IDE Support

The package includes type stubs (PEP 561) for IDE autocomplete and mypy type checking. All 200+ ArangoDB functions have full type hint support.

from aql_builder import AQLBuilder as AB

# IDE autocomplete works for all functions
AB.CONCAT(...)  # Shows function signature
AB.DATE_NOW()   # Shows return type

# Type checking with mypy
def build_query(min_age: int) -> str:
    return AB.for_('u').in_('users').filter(...).return_('u').to_aql()

Subqueries

The library provides built-in support for AQL subqueries with helper methods for common patterns.

IN Subquery

Check if a value exists in subquery results:

>>> from aql_builder import AQLBuilder as AB
>>>
>>> # Find users who have placed orders
>>> query = (
...     AB.for_('u').in_('users')
...     .filter(
...         AB.ref('u._id').in_subquery(
...             AB.for_('o').in_('orders').return_('o.userId')
...         )
...     )
...     .return_('u')
... )
>>> query.to_aql()
'FOR u IN users FILTER (u._id in (FOR o IN orders RETURN o.userId)) RETURN u'
>>>
>>> # Find users who have NOT placed orders
>>> query = (
...     AB.for_('u').in_('users')
...     .filter(
...         AB.ref('u._id').not_in_subquery(
...             AB.for_('o').in_('orders').return_('o.userId')
...         )
...     )
...     .return_('u')
... )
>>> query.to_aql()
'FOR u IN users FILTER (u._id not in (FOR o IN orders RETURN o.userId)) RETURN u'
>>>

Comparison Subqueries

Compare values against scalar subquery results (e.g., threshold from config):

>>> # Find products above threshold price from config
>>> query = (
...     AB.for_('p').in_('products')
...     .filter(
...         AB.ref('p.price').gt_subquery(
...             AB.for_('c').in_('config')
...             .filter(AB.ref('c.key').eq('premium_threshold'))
...             .return_('c.value')
...         )
...     )
...     .return_('p')
... )
>>> query.to_aql()
'FOR p IN products FILTER (p.price > (FOR c IN config FILTER (c.key == "premium_threshold") RETURN c.value)) RETURN p'
>>>

Other comparison methods: eq_subquery(), gte_subquery(), lt_subquery(), lte_subquery()

Aggregate Comparisons

For comparisons with aggregate functions (AVG, SUM, MIN, MAX), use the aggregate function directly:

>>> # Find products more expensive than average
>>> query = (
...     AB.for_('p').in_('products')
...     .filter(
...         AB.ref('p.price').gt(
...             AB.AVG(AB.for_('p2').in_('products').return_('p2.price'))
...         )
...     )
...     .return_('p')
... )
>>> query.to_aql()
'FOR p IN products FILTER (p.price > AVG((FOR p2 IN products RETURN p2.price))) RETURN p'
>>>

EXISTS Pattern

Check if a subquery returns any results:

>>> # Find users with recent activity
>>> query = (
...     AB.for_('u').in_('users')
...     .filter(
...         AB.exists_subquery(
...             AB.for_('a').in_('activities')
...             .filter(AB.ref('a.userId').eq('u._id'))
...             .return_('a')
...         )
...     )
...     .return_('u')
... )
>>> query.to_aql()
'FOR u IN users FILTER (LENGTH((FOR a IN activities FILTER (a.userId == u._id) RETURN a)) > 0) RETURN u'
>>>
>>> # Find users without activity
>>> query = (
...     AB.for_('u').in_('users')
...     .filter(
...         AB.not_exists_subquery(
...             AB.for_('a').in_('activities')
...             .filter(AB.ref('a.userId').eq('u._id'))
...             .return_('a')
...         )
...     )
...     .return_('u')
... )
>>> query.to_aql()
'FOR u IN users FILTER (LENGTH((FOR a IN activities FILTER (a.userId == u._id) RETURN a)) == 0) RETURN u'
>>>

Subquery Helpers

Get the first result or count from a subquery:

>>> # Get first value from subquery
>>> AB.first_from_subquery(
...     AB.for_('t').in_('thresholds').return_('t.value')
... ).to_aql()
'FIRST((FOR t IN thresholds RETURN t.value))'
>>>
>>> # Count subquery results
>>> AB.length_of_subquery(
...     AB.for_('o').in_('orders').return_('o')
... ).to_aql()
'LENGTH((FOR o IN orders RETURN o))'
>>>

License

This project is licensed under the Apache License 2.0 - see the LICENSE.txt file for details.

Acknowledgements

AQL builder is a free software project hosted at https://foss.heptapod.net. Thanks to the support of Clever Cloud, Octobus and the sponsors of the heptapod project.

Metadata

Release files for aql-builder 0.0.17

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aql-builder 0.0.17
File Size Uploaded
aql_builder-0.0.17.tar.gz 42.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aql-builder 0.0.17
File Interpreter ABI Platform
aql_builder-0.0.17-py3-none-any.whl Python 3 none any Details

Total release size: 72.7 kB

Release files / aql_builder-0.0.17.tar.gz

Download URL aql_builder-0.0.17.tar.gz
Size 42.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e5667af14aa7ee1885321e7b723c59cd53c4c0edcb9f01a31780e2e8a453251a
BLAKE2b-256 checksum
How to use checksums
535f974e492064c66d69bdd9ec656f86171c591f3640e5c91ae1cb0bc62a028f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.2

Release files / aql_builder-0.0.17-py3-none-any.whl

Download URL aql_builder-0.0.17-py3-none-any.whl
Size 30.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c547facae93165973852d278edd14eb2e54d69de6e91bed766fa75019c5c50f4
BLAKE2b-256 checksum
How to use checksums
614e16f47e66d85f1aac2557acfb42c234c6abe4ae0f216112804330d556931a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.2

Release history Release notifications | RSS feed

This release

0.0.17 This release

2 release files

0.0.16

2 release files

0.0.15

2 release files

0.0.14

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page