Skip to main content

ArangoDB AQL builder

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aql_builder-0.0.17.tar.gz (42.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aql_builder-0.0.17-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

File details

Details for the file aql_builder-0.0.17.tar.gz.

File metadata

  • Download URL: aql_builder-0.0.17.tar.gz
  • Upload date:
  • Size: 42.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for aql_builder-0.0.17.tar.gz
Algorithm Hash digest
SHA256 e5667af14aa7ee1885321e7b723c59cd53c4c0edcb9f01a31780e2e8a453251a
MD5 0d128ecc8fcb48f4ae197d98d4dd7a4c
BLAKE2b-256 535f974e492064c66d69bdd9ec656f86171c591f3640e5c91ae1cb0bc62a028f

See more details on using hashes here.

File details

Details for the file aql_builder-0.0.17-py3-none-any.whl.

File metadata

  • Download URL: aql_builder-0.0.17-py3-none-any.whl
  • Upload date:
  • Size: 30.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for aql_builder-0.0.17-py3-none-any.whl
Algorithm Hash digest
SHA256 c547facae93165973852d278edd14eb2e54d69de6e91bed766fa75019c5c50f4
MD5 656a74dfcf555fbe06b2fe602b5c2d8d
BLAKE2b-256 614e16f47e66d85f1aac2557acfb42c234c6abe4ae0f216112804330d556931a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page