Skip to content

Commit e40c900

Browse files
hyperpolymathclaude
andcommitted
TypedQLiser MVP: working SQL type checker (Levels 1-5)
Real implementation using sqlparser-rs: - Level 1: SQL parse validation (catches syntax errors) - Level 2: Schema binding (catches missing tables and columns) - Level 3: Type compatibility (catches type mismatches in operations) - Level 4: Null safety (warns on nullable columns without handling) - Level 5: Injection detection (flags string concatenation patterns) Plugin architecture: - QueryLanguagePlugin trait for language-agnostic checking - SqlPlugin implementation with PostgreSQL/MySQL/SQLite dialect support - Schema loading from JSON files (introspection planned) Working example: - examples/blog-api/ with schema.json, good-queries.sql, bad-queries.sql - Demonstrates pass/fail across levels with real SQL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4883657 commit e40c900

9 files changed

Lines changed: 840 additions & 49 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ anyhow = "1"
1919
thiserror = "2"
2020
walkdir = "2"
2121
glob = "0.3"
22+
sqlparser = "0.53"
2223

2324
[dev-dependencies]
2425
tempfile = "3"

examples/blog-api/bad-queries.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- These queries should FAIL at various levels
2+
3+
-- Level 1 failure: syntax error
4+
SELEC * FORM users;
5+
6+
-- Level 2 failure: nonexistent table
7+
SELECT * FROM nonexistent_table WHERE id = 1;
8+
9+
-- Level 2 failure: nonexistent column
10+
SELECT id, full_name FROM users;
11+
12+
-- Level 3 failure: comparing string to integer
13+
SELECT id FROM posts WHERE author_id = 'not_a_number';

examples/blog-api/good-queries.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- These queries should all pass levels 1-5
2+
3+
-- Simple select with known columns
4+
SELECT id, username, email FROM users WHERE id = $1;
5+
6+
-- Join with qualified column references
7+
SELECT p.title, u.username FROM posts p JOIN users u ON p.author_id = u.id WHERE p.published = true;
8+
9+
-- Parameterised insert (injection-safe)
10+
INSERT INTO comments (post_id, user_id, body) VALUES ($1, $2, $3);
11+
12+
-- Type-compatible comparison: integer = integer
13+
SELECT id, title FROM posts WHERE author_id = $1 AND published = true;

examples/blog-api/schema.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"tables": [
3+
{
4+
"name": "users",
5+
"columns": [
6+
{ "name": "id", "type": "serial", "nullable": false, "primary_key": true },
7+
{ "name": "username", "type": "varchar(50)", "nullable": false },
8+
{ "name": "email", "type": "varchar(255)", "nullable": false },
9+
{ "name": "bio", "type": "text", "nullable": true },
10+
{ "name": "created_at", "type": "timestamp", "nullable": false }
11+
]
12+
},
13+
{
14+
"name": "posts",
15+
"columns": [
16+
{ "name": "id", "type": "serial", "nullable": false, "primary_key": true },
17+
{ "name": "author_id", "type": "integer", "nullable": false },
18+
{ "name": "title", "type": "varchar(200)", "nullable": false },
19+
{ "name": "body", "type": "text", "nullable": false },
20+
{ "name": "published", "type": "boolean", "nullable": false },
21+
{ "name": "category", "type": "varchar(50)", "nullable": true },
22+
{ "name": "created_at", "type": "timestamp", "nullable": false }
23+
]
24+
},
25+
{
26+
"name": "comments",
27+
"columns": [
28+
{ "name": "id", "type": "serial", "nullable": false, "primary_key": true },
29+
{ "name": "post_id", "type": "integer", "nullable": false },
30+
{ "name": "user_id", "type": "integer", "nullable": false },
31+
{ "name": "body", "type": "text", "nullable": false },
32+
{ "name": "parent_id", "type": "integer", "nullable": true },
33+
{ "name": "created_at", "type": "timestamp", "nullable": false }
34+
]
35+
}
36+
]
37+
}

examples/blog-api/typedqliser.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# TypedQLiser example: blog API
2+
# Run: cd examples/blog-api && typedqliser check
3+
4+
[typedql]
5+
name = "blog-api"
6+
language = "sql"
7+
level = 4
8+
schema-source = "file"
9+
10+
[database]
11+
target-db = "postgresql"
12+
schema-file = "schema.json"
13+
14+
[paths]
15+
queries = ["*.sql"]
16+
embedding = "standalone"
17+
18+
[output]
19+
proof-certificates = false
20+
error-format = "human"
21+
22+
[levels]
23+
enforce = [1, 2, 3]
24+
warn = [4]
25+
skip = [5, 6, 7, 8, 9, 10]

0 commit comments

Comments
 (0)