{"slug": "show-hn-filtersql-a-dependency-free-json-to-sql-compiler-for-llms-and-webapis", "title": "Show HN: filtersql – A Dependency-Free JSON-to-SQL Compiler for LLMs and WebAPIs", "summary": "Filtersql, a dependency-free JSON-to-SQL compiler for LLMs and web APIs, converts structured JSON payloads into safe, parameterized SQL queries. Designed for DataTables backends, cursor-based pagination, and deterministic LLM pipelines, it supports PostgreSQL, SQLite, MySQL, and Oracle while protecting against SQL injection. The tool is stateless, driver-agnostic, and available via pip install filtersql.", "body_md": "Frontends, REST APIs and LLMs naturally produce structured JSON - not SQL.\n`filtersql`\n\ndefines a declarative JSON query language and compiles it\ninto safe, parameterized SQL.\n\n```\nJSON payload (or Python dicts) → filtersql → SQL string + values list\n```\n\nIt is intentionally not an ORM or a connection manager. It builds the query and returns it. Query execution remains the responsibility of the caller, making filtersql compatible with any Python DB driver.\n\nDesigned for three use cases:\n\n**DataTables** server-side backends**Cursor-based pagination**(Access-style, no OFFSET)** Deterministic LLM pipelines**- the LLM generates JSON, filtersql compiles it to safe SQL\n\nSupports PostgreSQL, SQLite, MySQL, and Oracle.\n\n**Secure by default**- Fully parameterized queries, protected against SQL injection** Multi-database support**- PostgreSQL, SQLite, MySQL, and Oracle** Lightweight**- No ORM required. Works with any database driver (psycopg2, sqlite3, mysql-connector, cx_Oracle, etc.)** Language-agnostic**- Clean JSON protocol, perfect for REST APIs and frontend applications** High-performance pagination**- Keyset (cursor-based) pagination, avoiding slow`OFFSET`\n\nqueries**AI/LLM friendly**- Designed for structured output from large language models\n\n`filtersql`\n\nis intentionally:\n\n- stateless\n- deterministic\n- driver agnostic\n- parameterized\n- declarative\n- portable\n- JSON-first\n- LLM friendly\n\nApplications describe what they want. `filtersql`\n\ndecides how to express it in SQL.\n\nFrom JSON:\n\n```\n{\n  \"action\": \"select\",\n  \"source\": \"users\",\n  \"filters\": [\n    {\n      \"field\": \"name\",\n      \"operator\": \"icontains\",\n      \"value\": \"john\"\n    }\n  ]\n}\n```\n\nto SQL:\n\n```\nselect\n  *\nfrom\n  \"users\"\nwhere\n  \"name\" ilike '%' || ? || '%'\n```\n\nwith values:\n\n```\n[\"john\"]\npip install filtersql\npython\nimport filtersql\n\nds = filtersql.Datasource(\n    source      = 'users',\n    dbms        = 'Pg',\n    placeholder = '%s',\n)\n\nquery, values = ds.select(\n    columns = [\n        {'field': 'id'},\n        {'field': 'first_name'},\n        {'field': 'last_name'},\n    ],\n    filters = [\n        {'field': 'first_name', 'operator': 'icontains', 'value': 'John'},\n        {'field': 'last_name',  'operator': 'icontains', 'value': 'Smith'},\n    ],\n    order = [{'field': 'first_name', 'order': 'asc'}],\n    limit = {'start': 0, 'length': 10},\n)\n\nprint(query)\n# select\n#   \"id\",\n#   \"first_name\",\n#   \"last_name\"\n# from\n#   \"users\"\n# where\n#   (\"first_name\" ilike '%' || %s || '%' and \"last_name\" ilike '%' || %s || '%')\n# order by\n#   \"first_name\" asc\n# limit 10 offset 0\n\nprint(values)\n# ['John', 'Smith']\n\ncursor.execute(query, values)\nrows = cursor.fetchall()\n```\n\nEvery filter is a dict with three keys:\n\n```\n{'field': 'status', 'operator': '=', 'value': 'active'}\n```\n\n`field`\n\n- column name, JSONB path (`attributes->>amount`\n\n), or schema-prefixed (`m.doc_type`\n\n)`operator`\n\n- the comparison operator (see table below)`value`\n\n- the value to compare against\n\nA flat list of filters is joined with AND:\n\n```\nfilters = [\n    {'field': 'status',   'operator': '=',         'value': 'active'},\n    {'field': 'doc_date', 'operator': '>=',        'value': '2025-01-01'},\n    {'field': 'title',    'operator': 'icontains', 'value': 'oxygen'},\n]\n```\n\nSome operators don't need a value:\n\n```\n{'field': 'deleted_at', 'operator': 'null'}\n{'field': 'deleted_at', 'operator': 'notnull'}\n```\n\nSome operators take a list:\n\n```\n{'field': 'status',   'operator': 'in',      'value': ['active', 'pending']}\n{'field': 'doc_date', 'operator': 'between', 'value': ['2025-01-01', '2025-12-31']}\n```\n\nWrap filters in a dict with key `'or'`\n\n:\n\n```\nfilters = [\n    {'field': 'status', 'operator': '=', 'value': 'active'},\n    {'or': [\n        {'field': 'first_name', 'operator': 'icontains', 'value': 'john'},\n        {'field': 'last_name',  'operator': 'icontains', 'value': 'john'},\n    ]},\n]\n# → \"status\" = ? AND (\"first_name\" like '%'||?||'%' OR \"last_name\" like '%'||?||'%')\n```\n\nOR groups can contain AND groups and vice versa:\n\n```\nfilters = [\n    {'or': [\n        {'field': 'doc_type', 'operator': '=', 'value': 'CONTRACT'},\n        {'and': [\n            {'field': 'doc_type', 'operator': '=',  'value': 'ORDER'},\n            {'field': 'amount',   'operator': '>=', 'value': '10000'},\n        ]},\n    ]},\n]\n# → (\"doc_type\" = ? OR (\"doc_type\" = ? AND \"amount\" >= ?))\n```\n\n`scope`\n\nis a dict of fixed `field: value`\n\npairs applied as `=`\n\nfilters to every operation on the Datasource - select, where, update, delete, and insert.\n\n```\nds = filtersql.Datasource(\n    source      = 'documents',\n    dbms        = 'Pg',\n    placeholder = '%s',\n    scope       = {'tenant_id': 42},\n)\n\n# scope is always added\nquery, values = ds.select(columns=columns, filters=user_filters)\n# → ... WHERE \"tenant_id\" = %s AND ...\n\nquery, values = ds.delete(id={'id': 99})\n# → ... WHERE \"id\" = %s AND \"tenant_id\" = %s\n\nquery, values = ds.insert(values={'title': 'New doc'})\n# → INSERT INTO \"documents\" (\"title\", \"tenant_id\") VALUES (%s, %s)\n```\n\nColumns are passed per call to `select()`\n\n, not at construction time - they can be computed at runtime.\n\nEach column is a dict with a `field`\n\nkey. Any extra keys (`label`\n\n, `visible`\n\n, `editable`\n\netc.) are ignored by filtersql and can be used by your frontend layer:\n\n```\ncolumns = [\n    {'field': 'id',         'label': 'ID',         'visible': True,  'editable': False},\n    {'field': 'first_name', 'label': 'First Name', 'visible': True,  'editable': True},\n    {'field': 'last_name',  'label': 'Last Name',  'visible': True,  'editable': True},\n]\n```\n\nPlain strings also work:\n\n```\ncolumns = ['id', 'first_name', 'last_name']\n```\n\nRaw expressions with `raw=True`\n\n:\n\n```\ncolumns = [\n    {'field': 'COUNT(*) as total', 'raw': True},\n    {'field': 'MAX(age) as max_age', 'raw': True},\n]\n```\n\nAll three return `(query, values)`\n\nlike `select()`\n\n.\n\n```\n# insert\nquery, values = ds.insert(\n    values = {'first_name': 'John', 'last_name': 'Smith'},\n)\n\n# insert with returning (PostgreSQL only)\nquery, values = ds.insert(\n    values    = {'first_name': 'John'},\n    returning = 'id',\n)\n\n# update - id can be single or composite key\nquery, values = ds.update(\n    id     = {'id': 42},\n    values = {'first_name': 'John', 'status': 'active'},\n)\n# values order: SET values first, then WHERE values\n# → ['John', 'active', 42]\n\n# delete\nquery, values = ds.delete(id={'id': 42})\n```\n\nWhen you want to inject filters into your own handwritten query:\n\n```\nds = filtersql.Datasource(source='documents', dbms='Pg', placeholder='%s')\n\nwhere_clause, values = ds.where(filters=[\n    {'field': 'doc_type', 'operator': '=',  'value': 'CONTRACT'},\n    {'field': 'doc_date', 'operator': '>=', 'value': '2025-01-01'},\n])\n# → '(\"doc_type\" = %s and \"doc_date\" >= %s)'\n# → ['CONTRACT', '2025-01-01']\n\nquery = f\"\"\"\n    SELECT v.chunk, m.doc_type\n    FROM file_vectors v\n    JOIN file_metadata m ON v.sha256 = m.sha256\n    WHERE v.model_name = %s\n    AND {where_clause}\n    ORDER BY v.embedding <=> %s\n\"\"\"\n```\n\nBuilds any query from a single payload dict. Useful for REST APIs and AI-generated actions:\n\n``` python\nfrom filtersql import filtersql\n\nquery, values = filtersql(\n    payload = {\n        'action':  'select',\n        'source':  'users',\n        'columns': [{'field': 'id'}, {'field': 'first_name'}],\n        'filters': [{'field': 'status', 'operator': '=', 'value': 'active'}],\n        'order':   [{'field': 'id', 'order': 'asc'}],\n        'limit':   {'start': 0, 'length': 10},\n    },\n    dbms        = 'Pg',\n    placeholder = '%s',\n)\n```\n\nSupported actions: `select`\n\n, `insert`\n\n, `update`\n\n, `delete`\n\n.\n\nReplaces placeholders with actual values for logging. Never use the output as real SQL.\n\n```\nquery, values = ds.select(columns=columns, filters=filters)\nprint(ds.debug(query, values))\n# select\n#   \"id\",\n#   \"first_name\"\n# from\n#   \"users\"\n# where\n#   (\"first_name\" ilike '%' || 'John' || '%')\n```\n\nLLMs should never generate SQL directly. They should generate structured intent. `filtersql`\n\nvalidates that intent and compiles it into parameterized SQL.\nThe values are always parameterized - no SQL injection risk even if the model produces unexpected output.\n\n```\nuser question → LLM → JSON → filtersql → parameterized SQL\n# DANGEROUS!\n# the naive approach everyone starts with\nquery = f\"SELECT * FROM users WHERE {llm_output}\"  # SQL injection waiting to happen\n\n# with filtersql\npayload = json.loads(llm_response)\nquery, values = filtersql(payload, dbms='Pg', placeholder='%s')\ncursor.execute(query, values)  # always parameterized, always safe\n```\n\nThe simplest version without Pydantic:\n\n``` python\nimport json\nfrom google import genai\nfrom filtersql import filtersql\n\nSCHEMA = {\n    \"type\": \"OBJECT\",\n    \"properties\": {\n        \"source\": {\"type\": \"STRING\", \"enum\": [\"users\", \"contracts\"]},\n        \"filters\": {\n            \"type\": \"ARRAY\",\n            \"items\": {\n                \"type\": \"OBJECT\",\n                \"properties\": {\n                    \"field\":    {\"type\": \"STRING\"},\n                    \"operator\": {\"type\": \"STRING\", \"enum\": [\"=\", \"!=\", \">\", \">=\", \"<\", \"<=\", \"icontains\", \"in\"]},\n                    \"value\":    {\"type\": \"STRING\"}\n                },\n                \"required\": [\"field\", \"operator\", \"value\"]\n            }\n        }\n    },\n    \"required\": [\"source\", \"filters\"]\n}\n\nresponse = client.models.generate_content(\n    model='gemini-flash',\n    contents=user_question,\n    config=genai.types.GenerateContentConfig(\n        system_instruction=\"Convert user questions into SQL filter payloads.\",\n        response_mime_type=\"application/json\",\n        response_schema=SCHEMA,\n        temperature=0.0,\n    )\n)\n\npayload = json.loads(response.text)\nquery, values = filtersql(payload, action='select', dbms='SQLite', placeholder='?')\n```\n\nWith Pydantic for stricter validation:\n\n``` python\nfrom pydantic import BaseModel, Field\nfrom typing import List, Literal\n\nclass SQLFilter(BaseModel):\n    field: Literal['doc_type', 'doc_date', 'author', 'title']\n    operator: Literal['=', '!=', '>', '>=', '<', '<=', 'icontains', 'between', 'in', 'notin']\n    value: str\n\nclass QuerySchema(BaseModel):\n    semantic_query: str\n    sql_filters: List[SQLFilter]\n\nparsed = QuerySchema.model_validate_json(response.text)\nfilters = [f.model_dump() for f in parsed.sql_filters]\n\nds = filtersql.Datasource(source='documents', dbms='Pg', placeholder='%s')\nwhere_clause, values = ds.where(filters=filters)\nphp\n{'field': 'attributes->>amount',  'operator': '>=', 'value': '10000',     'value_type': 'numeric'}\n# → (\"attributes\"->>'amount')::numeric >= %s::numeric\n\n{'field': 'attributes->>expiry',  'operator': '<=', 'value': '2025-12-31', 'value_type': 'date'}\n# → (\"attributes\"->>'expiry')::date <= %s::date\n```\n\nWithout the cast, `'9' > '10'`\n\nas text gives wrong results for numeric ranges.\n\nNavigates large tables without OFFSET - no performance degradation.\n\n```\nds = filtersql.Datasource(\n    source = 'documents',\n    dbms   = 'Pg',\n    order  = [{'field': 'id', 'order': 'asc'}],\n)\n\n# forward\nquery, values = ds.select(\n    columns   = columns,\n    cursor    = {'id': last_seen_id},\n    direction = 'next',\n)\n\n# backward\nquery, values = ds.select(\n    columns   = columns,\n    cursor    = {'id': last_seen_id},\n    direction = 'prev',\n)\n\n# jump to specific record\nquery, values = ds.select(\n    columns   = columns,\n    cursor    = {'id': 42},\n    direction = 'seek',\n)\n```\n\n`direction` |\nSQL | Use case |\n|---|---|---|\n`'next'` |\n`field > last_value` |\nForward navigation |\n`'prev'` |\n`field < last_value` |\nBackward navigation |\n`'seek'` |\n`field = value` |\nJump to specific record |\n\nMulti-column cursors work too:\n\n```\nquery, values = ds.select(\n    columns   = columns,\n    cursor    = {'first_name': last_first, 'last_name': last_last},\n    direction = 'next',\n)\n# pre-built tsvector column\n{'field': 'tsv_content', 'operator': 'fts', 'value': 'oxygen supply'}\n# → \"tsv_content\" @@ websearch_to_tsquery('english', %s)\n\n# on the fly from a text column\n{'field': 'description', 'operator': 'fts_query', 'value': 'oxygen supply'}\n# → to_tsvector('english', coalesce(\"description\", '')) @@ websearch_to_tsquery('english', %s)\n\nds = filtersql.Datasource(..., fts_language='italian')\n{'field': 'content', 'operator': 'fts', 'value': 'oxygen supply'}\n# → match(\"content\") against(? in boolean mode)\n```\n\n| Operator | Description |\n|---|---|\n`=` `!=` `>` `>=` `<` `<=` |\nComparison |\n`between` |\nRange - value must be `[low, high]` |\n`contains` |\nCase-sensitive substring |\n`starts_with` |\nCase-sensitive prefix |\n`ends_with` |\nCase-sensitive suffix |\n`not_contains` |\nCase-sensitive substring exclusion |\n`not_starts_with` |\nCase-sensitive prefix exclusion |\n`icontains` |\nCase-insensitive substring |\n`istarts_with` |\nCase-insensitive prefix |\n`iends_with` |\nCase-insensitive suffix |\n`not_icontains` |\nCase-insensitive substring exclusion |\n`null` / `notnull` |\nNULL checks - no value needed |\n`in` / `notin` |\nList membership |\n`reverse_in` |\nValue in a set of columns - `? IN (col1, col2)` |\n`regexp` |\nCase-sensitive regular expression |\n`iregexp` |\nCase-insensitive regexp (Pg: `~*` , Oracle: `regexp_like` with `i` ) |\n`fts` |\nFull-text search on indexed column (Pg, MySQL) |\n`fts_query` |\nFull-text search on text column (Pg only) |\n\n| DBMS | `dbms` value |\nDefault placeholder |\n|---|---|---|\n| PostgreSQL | `'Pg'` |\n`%s` |\n| SQLite | `'SQLite'` |\n`?` |\n| MySQL | `'mysql'` |\n`?` |\n| Oracle | `'Oracle'` |\n`?` |\n\nOverride with any placeholder your driver expects:\n\n```\nds = filtersql.Datasource(..., placeholder='%s')\nds = filtersql.Datasource(..., placeholder=':val')\n```\n\nWorking examples are in the `/examples`\n\nfolder:\n\n`examples/datatables/`\n\n- Flask + DataTables server-side with column filters and global search`examples/pagination/`\n\n- REST API gateway with security whitelist for insert/update/delete`examples/ai/`\n\n- Gemini structured output → filtersql → SQLite\n\nMIT", "url": "https://wpnews.pro/news/show-hn-filtersql-a-dependency-free-json-to-sql-compiler-for-llms-and-webapis", "canonical_source": "https://github.com/fthiella/filtersql/", "published_at": "2026-07-24 08:56:45+00:00", "updated_at": "2026-07-24 09:22:30.729942+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "generative-ai"], "entities": ["filtersql", "PostgreSQL", "SQLite", "MySQL", "Oracle"], "alternates": {"html": "https://wpnews.pro/news/show-hn-filtersql-a-dependency-free-json-to-sql-compiler-for-llms-and-webapis", "markdown": "https://wpnews.pro/news/show-hn-filtersql-a-dependency-free-json-to-sql-compiler-for-llms-and-webapis.md", "text": "https://wpnews.pro/news/show-hn-filtersql-a-dependency-free-json-to-sql-compiler-for-llms-and-webapis.txt", "jsonld": "https://wpnews.pro/news/show-hn-filtersql-a-dependency-free-json-to-sql-compiler-for-llms-and-webapis.jsonld"}}