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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a5e5263b1d4850899c9bac3de5fc9569ff79fc5b5abc3ca54f1404444488dba
|
|
| MD5 |
e38fd6cd4c2e757cec19c06710a5255c
|
|
| BLAKE2b-256 |
24bcc1db79dff273fabc57710c4f936ce83339e18e1ba686756aa05f769d8920
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b02ed41e79634ea2b07e5bfcefc598e69ff19e4665d7636894a40fdaf9f7be61
|
|
| MD5 |
6fb709f4a29ffa6423050af18c400486
|
|
| BLAKE2b-256 |
9eef56f3cacf85863aa3b4b3c2e3cf5eb08313a696d9d597e40840185e5df50e
|