Source code for neosqlite.collection.query_helper.schema_compiler
"""
SQL Compiler for JSON Schema validation.
Translates MongoDB $jsonSchema rules into native SQLite SQL expressions.
"""
from typing import Any
[docs]
def compile_schema_to_sql(
schema: dict[str, Any], data_column: str = "data", jsonb: bool = False
) -> str:
"""
Compile a JSON Schema into a SQL expression for use in a CHECK constraint.
"""
json_func = "jsonb_extract" if jsonb else "json_extract"
return _compile_node(schema, data_column, json_func)
[docs]
def _compile_node(
schema: Any, data_column: str, json_func: str, path: str = "$"
) -> str:
"""Recursively compile schema nodes into SQL."""
if not isinstance(schema, dict):
return "1" # Always true
clauses = []
# 1. Handle required fields
if "required" in schema:
req_fields = schema["required"]
for field in req_fields:
field_path = f"{path}.{field}"
clauses.append(
f"{json_func}({data_column}, '{field_path}') IS NOT NULL"
)
# 2. Handle properties
if "properties" in schema:
for field, prop_schema in schema["properties"].items():
field_path = f"{path}.{field}"
# Only validate if the field exists (JSON Schema standard behavior)
prop_sql = _compile_node(
prop_schema, data_column, json_func, field_path
)
if prop_sql != "1":
clauses.append(
f"({json_func}({data_column}, '{field_path}') IS NULL OR ({prop_sql}))"
)
# 3. Handle type/bsonType
# SQLite json_type returns: 'null', 'integer', 'real', 'text', 'array', 'object'
target_type = schema.get("bsonType") or schema.get("type")
if target_type:
type_clause = _compile_type_check(
data_column, path, target_type, json_func
)
if type_clause:
clauses.append(type_clause)
# 4. Handle numeric constraints
val_expr = f"{json_func}({data_column}, '{path}')"
if "minimum" in schema:
clauses.append(f"{val_expr} >= {schema['minimum']}")
if "maximum" in schema:
clauses.append(f"{val_expr} <= {schema['maximum']}")
if "exclusiveMinimum" in schema:
clauses.append(f"{val_expr} > {schema['exclusiveMinimum']}")
if "exclusiveMaximum" in schema:
clauses.append(f"{val_expr} < {schema['exclusiveMaximum']}")
if not clauses:
return "1"
return " AND ".join(clauses)
[docs]
def _compile_type_check(
data_column: str, path: str, target_type: Any, json_func: str
) -> str | None:
"""Compile type checks into SQL using json_type."""
# json_type(data, path)
type_expr = f"json_type({data_column}, '{path}')"
types = target_type if isinstance(target_type, list) else [target_type]
sql_types = []
for t in types:
match t:
case "string":
sql_types.append("'text'")
case "number":
sql_types.extend(["'integer'", "'real'"])
case "integer" | "int" | "long":
sql_types.append("'integer'")
case "double" | "decimal":
sql_types.append("'real'")
case "object":
sql_types.append("'object'")
case "array":
sql_types.append("'array'")
case "bool" | "boolean":
# SQLite stores bools as 0/1, json_type might not distinguish well
# but usually it's handled.
pass
case "null":
sql_types.append("'null'")
if not sql_types:
return None
return f"{type_expr} IN ({', '.join(sql_types)})"