Skip to main content

sql-query-tagger

Static SQL query classifier and injection-risk analyzer for all Amazon RDS database engines.

sql-query-tagger parses a SQL query string and tells you:

  • Query type: DDL, DML, DQL, DCL, TCL, UTILITY, PROCEDURAL, ADMIN, or UNKNOWN
  • Security risk: LOW / MEDIUM / HIGH / CRITICAL, with the specific patterns that triggered it (injection patterns, stacked queries, destructive DDL, engine-specific dangerous functions/catalogs)

No database connection required — this is purely static analysis over the query text.

Supported engines

Engine Versions sqlglot dialect
PostgreSQL 11-17 postgres
Aurora PostgreSQL 11-16 postgres
MySQL 5.7, 8.0 mysql
Aurora MySQL 5.7, 8.0 mysql
MariaDB 10.6, 10.11, 11.4 mysql
Oracle 19c, 21c, 23ai oracle
SQL Server 2017, 2019, 2022 tsql

Listed versions are the ones with version-specific pattern tuning. Other versions of a supported engine still work — they fall back to the engine's base pattern set with a warning logged.

Install

pip install sql-query-tagger

Quickstart

from sql_query_tagger import SQLClassifier

classifier = SQLClassifier(engine="postgresql", version="16")
result = classifier.classify_query("SELECT * FROM users WHERE id = 1 OR 1=1")

print(result.query_type)                       # QueryType.DQL
print(result.security_analysis.risk_level)     # RiskLevel.HIGH
print(result.security_analysis.detected_patterns)
print(result.security_analysis.recommendation)

More examples

Blocking a destructive statement

from sql_query_tagger import SQLClassifier

classifier = SQLClassifier(engine="mysql", version="8.0")
result = classifier.classify_query("SELECT 1; DROP TABLE users;")

print(result.security_analysis.risk_level)         # RiskLevel.CRITICAL
print(result.security_analysis.is_suspicious)      # True
print(result.security_analysis.detected_patterns)
# ['stacked_queries_multiple_statements', 'stacked_queries_dangerous_followup', ...]

Engine-specific dangerous function

classifier = SQLClassifier(engine="sqlserver", version="2022")
result = classifier.classify_query("EXEC xp_cmdshell 'dir'")

print(result.security_analysis.engine_specific_risks)
# ['dangerous_function_xp_cmdshell']

Classifying query type only (DDL/DML/DQL/...)

classifier = SQLClassifier(engine="postgresql", version="16")

for sql in ["CREATE TABLE t (id INT)", "INSERT INTO t VALUES (1)", "SELECT * FROM t"]:
    result = classifier.classify_query(sql)
    print(sql, "->", result.query_type)
# CREATE TABLE t (id INT) -> QueryType.DDL
# INSERT INTO t VALUES (1) -> QueryType.DML
# SELECT * FROM t -> QueryType.DQL

Handling invalid input and unknown engines

from sql_query_tagger import SQLClassifier, UnsupportedEngineError

try:
    classifier = SQLClassifier(engine="db2", version="11.5")
except UnsupportedEngineError as e:
    print(f"Unsupported engine: {e}")

classifier = SQLClassifier(engine="postgresql", version="16")
try:
    classifier.classify_query("")
except ValueError as e:
    print(f"Invalid query: {e}")

Performance

classify_query memoizes its security analysis and query-type classification per SQLClassifier instance, keyed by the cleaned query text (this is the part that calls into sqlglot and runs the regex scans). Real traffic tends to repeat the same templated query shapes (an ORM or app issuing the same statement with different parameter values folded out), so this cache turns repeats into a dict lookup instead of a re-parse. Tune or disable it with SQLClassifier(..., analysis_cache_size=N) (0 disables caching).

Run the benchmark yourself:

python benchmarks/bench_classify.py

Measured on a single thread, 10,000 queries per scenario:

Scenario Throughput Latency
Repeated templated queries (cache hits) ~55,500 q/s ~0.018 ms/query
All-unique queries (cache misses, sqlglot parses every query) ~4,000 q/s ~0.25 ms/query

The unique-query case is the floor — it's bound by sqlglot's parser, which accounts for roughly 75% of per-call time (confirmed via cProfile). Since correct query-type classification depends on that parse, this floor isn't something sql-query-tagger can optimize away without giving up accuracy; the cache is what makes high-repeat production traffic fast.

Testing

pip install -e ".[dev]"
pytest --cov=sql_query_tagger --cov-report=term-missing

Test report

The suite has 114 tests across the classifier pipeline, every engine profile, the registry, and the public package API, with 99% statement coverage:

Name                                      Stmts   Miss  Cover   Missing
-----------------------------------------------------------------------
sql_query_tagger\__init__.py                        4      0   100%
sql_query_tagger\classifier.py                    132      1    99%   188
sql_query_tagger\engines\__init__.py                0      0   100%
sql_query_tagger\engines\aurora_mysql.py            6      0   100%
sql_query_tagger\engines\aurora_postgresql.py       6      0   100%
sql_query_tagger\engines\base.py                   32      0   100%
sql_query_tagger\engines\mariadb.py                 8      0   100%
sql_query_tagger\engines\mysql.py                  19      0   100%
sql_query_tagger\engines\oracle.py                 15      0   100%
sql_query_tagger\engines\postgresql.py             15      0   100%
sql_query_tagger\engines\registry.py               25      0   100%
sql_query_tagger\engines\sqlserver.py              15      0   100%
sql_query_tagger\exceptions.py                      1      0   100%
sql_query_tagger\types.py                          30      0   100%
-----------------------------------------------------------------------
TOTAL                                       308      1    99%
114 passed in 0.54s

The one uncovered line is a defensive fallback for a CTE-only top-level parse shape that sqlglot does not currently produce in practice.

Limitations

  • Static analysis only — it inspects query text, not a live schema, so it cannot tell you whether referenced tables/columns exist.
  • The stacked-query check splits on ; without parsing string literals, so a benign query containing a semicolon inside a string (e.g. 'a; b') may be flagged as multiple statements. Treat the risk score as a signal, not a verdict.

License

Apache License 2.0

Download files

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

Source Distribution

sql_query_tagger-0.1.1.tar.gz (47.5 kB view details)

Uploaded Source

Built Distribution

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

sql_query_tagger-0.1.1-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file sql_query_tagger-0.1.1.tar.gz.

File metadata

  • Download URL: sql_query_tagger-0.1.1.tar.gz
  • Upload date:
  • Size: 47.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for sql_query_tagger-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6a5e5263b1d4850899c9bac3de5fc9569ff79fc5b5abc3ca54f1404444488dba
MD5 e38fd6cd4c2e757cec19c06710a5255c
BLAKE2b-256 24bcc1db79dff273fabc57710c4f936ce83339e18e1ba686756aa05f769d8920

See more details on using hashes here.

File details

Details for the file sql_query_tagger-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: sql_query_tagger-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 20.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for sql_query_tagger-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b02ed41e79634ea2b07e5bfcefc598e69ff19e4665d7636894a40fdaf9f7be61
MD5 6fb709f4a29ffa6423050af18c400486
BLAKE2b-256 9eef56f3cacf85863aa3b4b3c2e3cf5eb08313a696d9d597e40840185e5df50e

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