-
Notifications
You must be signed in to change notification settings - Fork 50
Expand file tree
/
Copy pathsql-classify.ts
More file actions
73 lines (64 loc) · 2.94 KB
/
sql-classify.ts
File metadata and controls
73 lines (64 loc) · 2.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// altimate_change start — SQL query classifier for write detection
//
// Uses altimate-core's AST-based getStatementTypes() for accurate classification.
// Handles CTEs, string literals, procedural blocks, all dialects correctly.
// Lazy-loads altimate-core on first use to avoid crashing at import time
// when the native binary is unavailable (e.g. GLIBC mismatch).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let _core: any = null
function getCore(): any {
if (!_core) {
try {
_core = require("@altimateai/altimate-core")
} catch {
// Native binding unavailable — return null so callers can degrade gracefully
return null
}
}
return _core
}
// Categories from altimate-core that indicate write operations
const WRITE_CATEGORIES = new Set(["dml", "ddl", "dcl", "tcl"])
// Only SELECT queries are known safe. "other" (SHOW, SET, USE, etc.) is ambiguous — prompt for permission.
const READ_CATEGORIES = new Set(["query"])
// Hard-deny patterns — blocked regardless of permissions
const HARD_DENY_TYPES = new Set(["DROP DATABASE", "DROP SCHEMA", "TRUNCATE", "TRUNCATE TABLE"])
/**
* Classify a SQL string as "read" or "write" using AST parsing.
* If ANY statement is a write, returns "write".
* Falls back to "write" (safe default) if native binding is unavailable.
*/
export function classify(sql: string): "read" | "write" {
const core = getCore()
if (!core) return "write" // fail-safe: treat as write when native unavailable
const result = core.getStatementTypes(sql)
if (!result?.categories?.length) return "read"
// Treat unknown categories (not in WRITE or READ sets) as write to fail safe
return result.categories.some((c: string) => !READ_CATEGORIES.has(c)) ? "write" : "read"
}
/**
* Classify a multi-statement SQL string.
* getStatementTypes handles multi-statement natively — no semicolon splitting needed.
*/
export function classifyMulti(sql: string): "read" | "write" {
return classify(sql)
}
/**
* Single-pass: classify and check for hard-denied statement types.
* Returns both the overall query type and whether a hard-deny pattern was found.
* Falls back to write + not-blocked when native binding is unavailable.
*/
export function classifyAndCheck(sql: string): { queryType: "read" | "write"; blocked: boolean } {
const core = getCore()
if (!core) return { queryType: "write", blocked: false }
const result = core.getStatementTypes(sql)
if (!result?.statements?.length) return { queryType: "read", blocked: false }
const blocked = result.statements.some((s: { statement_type: string }) =>
s.statement_type && HARD_DENY_TYPES.has(s.statement_type.toUpperCase()),
)
const categories = result.categories ?? []
// Unknown categories (not in WRITE or READ sets) are treated as write to fail safe
const queryType = categories.some((c: string) => !READ_CATEGORIES.has(c)) ? "write" : "read"
return { queryType: queryType as "read" | "write", blocked }
}
// altimate_change end