Skip to main content

Module Standards

This page defines the mandatory structure every community module must follow — the module.json schema, module types, and the required documentation format.


Module Types

Every module must declare its type in module.json. The type determines which interface methods must be implemented and how the module is loaded.

standalone

A complete database implementation. Handles its own connection, translation, execution, and schema discovery. Users can point a connection directly at this module.

{ "type": "standalone" }

Required interface methods: all of UQALModule.

Examples: standard.postgresql, standard.mongodb, standard.neo4j

extension

Adds grammar or capabilities to an existing primary module. Does not provide its own connection — it must be loaded alongside the module it extends.

{ "type": "extension", "extends": "standard.postgresql" }

Required interface methods: get_manifest, get_grammar_extension, get_capabilities. All other methods may raise NotImplementedError.

Examples: community.postgis (extends PostgreSQL), community.pgvector

The extends field is used by:

  • The loader to verify the primary module is present before loading the extension
  • The documentation site to group the extension under its primary module
  • CI to validate that the primary module's interface version is compatible

module.json Schema

Every module directory must contain a module.json at its root.

{
"name": "community.postgis",
"version": "0.2.0",
"compatible_with": 1,
"type": "extension",
"extends": "standard.postgresql",
"description": "PostGIS geospatial syntax for PostgreSQL connections.",
"author": "Your Name",
"repository": "https://github.com/uqal/uqal-modules"
}

Field Reference

FieldRequiredDescription
nameModule identifier. Must use a valid prefix (community.*, local.*).
versionSemantic version string: MAJOR.MINOR.PATCH.
compatible_withInteger. Must match the current UQALModule.INTERFACE_VERSION.
type"standalone" or "extension".
extendsif extensionThe name of the primary module this extension depends on.
descriptionOne-sentence summary. Shown in uqal list-modules and the docs index.
authorName or GitHub handle of the module author.
repositoryURL of the source repository.

Version Bumping

Do not set version manually. CI writes the version automatically on every merge to main, based on the PR title type:

PR typeBumpExample
breakingMajor1.0.0 → 2.0.0
featMinor0.1.0 → 0.2.0
fix, refactorPatch0.1.1 → 0.1.2
docs, testNoneNo publish triggered

The version in module.json is updated by CI before the package is built. You only need to ensure the field exists in the file — its value will be overwritten. See Community Workflow for the full automatic versioning flow.

compatible_with

The compatible_with value must equal UQALModule.INTERFACE_VERSION. This integer is bumped only when the UQALModule interface itself changes in a breaking way (e.g. a required method is renamed or its signature changes).

CI blocks the PR if compatible_with does not match the current interface version.


Naming Conventions

ScopePrefixExample
Built-in (maintained by core team)standard.*standard.postgresql
Community (third-party, in uqal-modules)community.*community.postgis
Private / company-internallocal.*local.mycompany_db

Prefixes are a convention, not enforced by the loader. Using them consistently makes it immediately clear where a module comes from.


Required Test Structure

Every module must include a tests/ folder. CI checks for the required files and runs coverage before a PR can be merged.

modules/community.postgis/
├── module.json
├── __init__.py
├── translator.py
├── native_validator.py
├── grammar_extension.lark (if grammar extension)
├── docker-compose.test.yml (mandatory for standalone, recommended for extension)
├── tests/
│ ├── unit/
│ │ ├── test_translator.py ← mandatory
│ │ └── test_validator.py ← mandatory if native_validator.py exists
│ ├── grammar/
│ │ └── test_grammar.py ← mandatory if grammar_extension.lark exists
│ └── integration/
│ └── test_integration.py ← mandatory for standalone, recommended for extension
└── docs/
├── index.md
└── syntax.md

Coverage Requirements

FileMinimum coverageReason
translator.py80%Core translation logic
native_validator.py90%Security-critical
grammar_extension.lark100% of rulesEvery rule needs at least one grammar test
connection_schema.pyno thresholdTrivial field declarations
__init__.pyno thresholdTrivial

How to Write Tests: Output-Based Testing

The core idea: assert what comes out, not what happens inside the database.

Every test verifies the output of the translator — the query string and parameters it generates — without a real database connection. This makes tests fast, deterministic, and runnable in any CI environment.


Unit Tests — Translator Output (tests/unit/test_translator.py)

import pytest
from unittest.mock import MagicMock
from uqal_core.ast.nodes import DbTableCall, Compare, VariableRef, StringLiteral
from community_postgis.translator import PostGISTranslator

@pytest.fixture
def translator():
return PostGISTranslator()

# ── Translation output ────────────────────────────────────────────────────────

def test_st_within_generates_correct_sql(translator):
node = STWithinNode(column="geom", polygon="POLYGON((0 0,1 0,1 1,0 0))")
query, params = translator.translate(node)
assert "ST_Within(geom, $1)" in query
assert list(params.values())[0] == "POLYGON((0 0,1 0,1 1,0 0))"

def test_st_distance_returns_parameterised_value(translator):
node = STDistanceNode(col_a="point_a", col_b="point_b")
query, params = translator.translate(node)
assert "ST_Distance(point_a, point_b)" in query
assert params == {} # no user-supplied value → no params

def test_get_table_with_spatial_filter(translator):
# Full UQAL node tree: get_table(where ST_Within(geom, '...'))
condition = STWithinNode(column="geom", polygon="POLYGON((...))")
node = DbTableCall(table="locations", command="get_table", where=condition)
query, params = translator.translate(node)
assert query.startswith("SELECT")
assert "ST_Within" in query
assert len(params) == 1

# ── Edge cases ────────────────────────────────────────────────────────────────

def test_null_polygon_raises(translator):
with pytest.raises(ValueError, match="polygon"):
translator.translate(STWithinNode(column="geom", polygon=None))

def test_empty_column_raises(translator):
with pytest.raises(ValueError):
translator.translate(STWithinNode(column="", polygon="POLYGON((...))"))

Unit Tests — Native Validator (tests/unit/test_validator.py)

from community_postgis.native_validator import security_check

# ── Blocked patterns ──────────────────────────────────────────────────────────

def test_drop_table_is_blocked():
errors = security_check("DROP TABLE locations")
assert len(errors) > 0
assert any("DROP" in e for e in errors)

def test_truncate_is_blocked():
errors = security_check("TRUNCATE locations")
assert len(errors) > 0

# ── Allowed patterns ──────────────────────────────────────────────────────────

def test_select_with_st_within_is_allowed():
errors = security_check("SELECT * FROM locations WHERE ST_Within(geom, $1)")
assert errors == []

def test_select_with_st_distance_is_allowed():
errors = security_check("SELECT ST_Distance(a, b) FROM points")
assert errors == []

# ── Case insensitive ──────────────────────────────────────────────────────────

def test_drop_lowercase_is_blocked():
errors = security_check("drop table locations")
assert len(errors) > 0

Unit Tests — Connection / Auth Extensions (tests/unit/test_translator.py)

For auth extensions (Kerberos, mTLS, etc.) there is no query to assert. Instead, mock the driver and assert it is called with the correct parameters.

from unittest.mock import MagicMock, patch
from community_mongo_kerberos import KerberosExtension

def test_kerberos_passes_gssapi_mechanism():
module = KerberosExtension()
mock_driver = MagicMock()

with patch("community_mongo_kerberos.pymongo.MongoClient", mock_driver):
module.build_connection({"host": "db.example.com", "service": "mongodb"})

mock_driver.assert_called_once_with(
"db.example.com",
authMechanism="GSSAPI",
authSource="$external",
)

def test_kerberos_service_name_is_configurable():
module = KerberosExtension()
mock_driver = MagicMock()

with patch("community_mongo_kerberos.pymongo.MongoClient", mock_driver):
module.build_connection({"host": "db.example.com", "service": "myservice"})

call_kwargs = mock_driver.call_args.kwargs
assert call_kwargs["authMechanismProperties"] == "SERVICE_NAME:myservice"

Grammar Tests (tests/grammar/test_grammar.py)

Grammar tests verify that your new syntax parses to the right AST node type. No database, no translation — only parsing.

import pytest
from lark import Token
from uqal_core.grammar.builder import GrammarBuilder
from uqal_core.transformer import UQALTransformer
from standard_postgresql import PostgreSQLModule
from community_postgis import PostGISExtension
from community_postgis.nodes import STWithinNode, STDistanceNode

@pytest.fixture(scope="module")
def parse():
grammar = GrammarBuilder().build([PostgreSQLModule(), PostGISExtension()])
transformer = UQALTransformer()
def _parse(query):
tree = grammar.parse(query)
return transformer.transform(tree)
return _parse

# ── Parses to correct node type ───────────────────────────────────────────────

def test_st_within_produces_node(parse):
result = parse("mydb.locations.get_table(where ST_Within(geom, 'POLYGON(...)'))")
# Walk the result to find the condition node
condition = result.steps[0].where
assert isinstance(condition, STWithinNode)

def test_st_within_column_captured(parse):
result = parse("mydb.locations.get_table(where ST_Within(point_col, 'POLYGON(...)'))")
condition = result.steps[0].where
assert condition.column == "point_col"

def test_st_distance_produces_node(parse):
result = parse("mydb.points.get_table(where ST_Distance(a, b) < 100)")
condition = result.steps[0].where
assert isinstance(condition.left, STDistanceNode)

# ── Invalid syntax raises parse error ────────────────────────────────────────

def test_st_within_missing_polygon_raises(parse):
with pytest.raises(Exception):
parse("mydb.locations.get_table(where ST_Within(geom))")

def test_unknown_spatial_function_raises(parse):
with pytest.raises(Exception):
parse("mydb.locations.get_table(where ST_NotReal(geom, 'x'))")

Integration Tests (tests/integration/test_integration.py)

Only runs in CI when docker-compose.test.yml is present. Skipped otherwise.

import pytest
import os

# Skip entire file if no DB configured
pytestmark = pytest.mark.skipif(
os.getenv("TEST_DB_HOST") is None,
reason="Integration tests require TEST_DB_HOST environment variable"
)

def test_st_within_returns_results(pg_connection):
# pg_connection is a pytest fixture that connects to the test DB
result = uqal.run(
"testdb.locations.get_table(where ST_Within(geom, 'POLYGON((0 0,10 0,10 10,0 0))'))",
connection=pg_connection
)
assert len(result.rows) > 0

def test_st_within_excludes_out_of_bounds(pg_connection):
result = uqal.run(
"testdb.locations.get_table(where ST_Within(geom, 'POLYGON((90 90,91 90,91 91,90 90))'))",
connection=pg_connection
)
assert len(result.rows) == 0

Test Mandatory by Module Type

Teststandaloneextension
Unit tests (mock-based)MandatoryMandatory
Grammar testsMandatory if grammar presentMandatory (grammar is the main purpose)
Compliance suiteMandatoryMandatory
Integration testsOptional — run if docker-compose.test.yml presentOptional
docker-compose.test.ymlRecommendedOptional

Required Documentation Structure

Every module must include exactly two documentation files. Additional pages are allowed but optional.

modules/community.postgis/
└── docs/
├── index.md ← mandatory
├── syntax.md ← mandatory
└── advanced.md ← optional, any name

CI blocks the PR if docs/index.md or docs/syntax.md is missing, or if the mandatory sections (see below) are absent.


index.md — Required Sections

index.md is the main page a user lands on when they find your module.

---
title: PostGIS
---

# PostGIS (`community.postgis`)

One sentence: what this module is and what it extends.

## Overview

What problem does this module solve? What does it add to the primary module?
Two to four sentences.

## Prerequisites

What must be set up before this module works.
This section is mandatory — missing prerequisites are the most common
support issue for extension modules.

Example:
- PostGIS extension must be enabled: `CREATE EXTENSION postgis;`
- PostgreSQL >= 12

## Setup

How to install and register the module:

uv add uqal-postgis
uqal add-module community.postgis

Connection config example (module.json / uqal_config.json).

## Quick Example

One complete, copy-paste-ready example that shows the most common use case.

Mandatory section checklist

SectionRequired
## Overview
## Prerequisites
## Setup
## Quick Example

syntax.md — Required Sections

syntax.md is the syntax reference. One entry per new feature the module adds.

---
title: PostGIS Syntax Reference
---

# PostGIS Syntax Reference

## ST_Within

**Description**: Returns rows where the geometry column falls within
a given polygon.

**Syntax**:

db.table.get_table(where ST_Within(geom_column, polygon_wkt))

**Parameters**:

| Parameter | Type | Description |
|-----------|------|-------------|
| `geom_column` | column reference | The geometry column to test |
| `polygon_wkt` | string literal | WKT polygon string |

**Example**:

let results = mydb.locations.get_table(
where ST_Within(geom, "POLYGON((...))")
)

---

## ST_Distance

...

Mandatory section checklist per feature

ElementRequired
Feature heading
Description (one line)
Syntax block
Example
Parameters tableif the feature takes parameters

Optional Pages

Contributors may add as many additional pages as they want under docs/. Common additions:

PageWhen useful
advanced.mdAdvanced configuration, performance tuning
migration.mdHow to migrate from an older version of the module
type-mapping.mdFull mapping of database-native types to UQAL types
changelog.mdModule-specific changelog (auto-generated from git-cliff)

All pages in docs/ are automatically included in the documentation site under the module's section when the PR is merged to main.


Docusaurus Frontmatter

Every documentation page must start with a valid Docusaurus frontmatter block:

---
title: Page Title
---

Optional fields:

---
title: PostGIS Syntax Reference
sidebar_label: Syntax Reference
sidebar_position: 2
---

sidebar_position controls the order within the module's section. index.md should be position 1, syntax.md position 2.