sqyrl 0.0.2 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -4
- package/dist/index.d.mts +203 -40
- package/dist/index.mjs +5344 -1063
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,15 +15,44 @@ npm install sqyrl
|
|
|
15
15
|
```ts
|
|
16
16
|
import { sqyrl } from "sqyrl";
|
|
17
17
|
|
|
18
|
-
const sql = sqyrl(`SELECT id, name FROM
|
|
19
|
-
schema: "public",
|
|
18
|
+
const sql = sqyrl(`SELECT id, name FROM users WHERE status = 'active' LIMIT 10`, {
|
|
20
19
|
table: "users",
|
|
21
|
-
|
|
20
|
+
col: "tenant_id",
|
|
22
21
|
value: "acme",
|
|
23
22
|
});
|
|
24
23
|
|
|
25
24
|
console.log(sql);
|
|
26
|
-
// SELECT id, name
|
|
25
|
+
// SELECT id, name
|
|
26
|
+
// FROM users
|
|
27
|
+
// WHERE (users.tenant_id = 'acme'
|
|
28
|
+
// AND status = 'active') LIMIT 10
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or, more usefully:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { makeSqyrl } from "sqyrl";
|
|
35
|
+
import { tool } from "ai";
|
|
36
|
+
import { sql } from "drizzle-orm";
|
|
37
|
+
import { db } from "@/db";
|
|
38
|
+
|
|
39
|
+
function makeSqlTool(orgId: string) {
|
|
40
|
+
// Create a sanitiser function for this tenant
|
|
41
|
+
const sqyrl = makeSqyrl({ table: "org", col: "id", value: orgId });
|
|
42
|
+
|
|
43
|
+
return tool({
|
|
44
|
+
description: "Run raw SQL against the DB",
|
|
45
|
+
inputSchema: z.object({ query: z.string() }),
|
|
46
|
+
execute: async ({ query }) => {
|
|
47
|
+
// The LLM can pass any query it likes, we'll sanitise it if possible
|
|
48
|
+
// and return helpful error messages if not
|
|
49
|
+
const sanitised = sqyrl(query);
|
|
50
|
+
// Now we can throw that straight at the db and be confident it'll only
|
|
51
|
+
// return data from the specified tenant
|
|
52
|
+
return db.execute(sql.raw(sanitised));
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
27
56
|
```
|
|
28
57
|
|
|
29
58
|
`sqyrl` parses the SQL, enforces a mandatory equality filter on the given column as the outermost `AND` condition (so it cannot be short-circuited by agent-supplied `OR` clauses), and returns the sanitised SQL string.
|
|
@@ -35,12 +64,24 @@ console.log(sql);
|
|
|
35
64
|
|
|
36
65
|
## Development
|
|
37
66
|
|
|
67
|
+
First install [Vite+](https://viteplus.dev/guide/):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl -fsSL https://vite.plus | bash
|
|
71
|
+
```
|
|
72
|
+
|
|
38
73
|
Install dependencies:
|
|
39
74
|
|
|
40
75
|
```bash
|
|
41
76
|
vp install
|
|
42
77
|
```
|
|
43
78
|
|
|
79
|
+
Format, lint, typecheck:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
vp check --fix
|
|
83
|
+
```
|
|
84
|
+
|
|
44
85
|
Run the unit tests:
|
|
45
86
|
|
|
46
87
|
```bash
|
package/dist/index.d.mts
CHANGED
|
@@ -1,44 +1,217 @@
|
|
|
1
1
|
//#region src/ast.d.ts
|
|
2
2
|
interface SelectStatement {
|
|
3
|
-
type: "select";
|
|
3
|
+
readonly type: "select";
|
|
4
|
+
distinct: Distinct | null;
|
|
4
5
|
columns: Column[];
|
|
5
|
-
from:
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
from: SelectFrom;
|
|
7
|
+
joins: JoinClause[];
|
|
8
|
+
where: WhereRoot | null;
|
|
9
|
+
groupBy: GroupByClause | null;
|
|
10
|
+
having: HavingClause | null;
|
|
11
|
+
orderBy: OrderByClause | null;
|
|
12
|
+
limit: LimitClause | null;
|
|
13
|
+
offset: OffsetClause | null;
|
|
8
14
|
}
|
|
9
|
-
|
|
15
|
+
interface Distinct {
|
|
16
|
+
readonly type: "distinct";
|
|
17
|
+
}
|
|
18
|
+
interface GroupByClause {
|
|
19
|
+
readonly type: "group_by";
|
|
20
|
+
items: WhereValue[];
|
|
21
|
+
}
|
|
22
|
+
interface HavingClause {
|
|
23
|
+
readonly type: "having";
|
|
24
|
+
expr: WhereExpr;
|
|
25
|
+
}
|
|
26
|
+
interface OrderByClause {
|
|
27
|
+
readonly type: "order_by";
|
|
28
|
+
items: OrderByItem[];
|
|
29
|
+
}
|
|
30
|
+
type SortDirection = "asc" | "desc";
|
|
31
|
+
type NullsOrder = "nulls_first" | "nulls_last";
|
|
32
|
+
interface OrderByItem {
|
|
33
|
+
readonly type: "order_by_item";
|
|
34
|
+
expr: WhereValue;
|
|
35
|
+
direction?: SortDirection;
|
|
36
|
+
nulls?: NullsOrder;
|
|
37
|
+
}
|
|
38
|
+
interface OffsetClause {
|
|
39
|
+
readonly type: "offset";
|
|
40
|
+
value: number;
|
|
41
|
+
}
|
|
42
|
+
type JoinType = "inner" | "inner_outer" | "left" | "left_outer" | "right" | "right_outer" | "full" | "full_outer" | "cross" | "natural";
|
|
43
|
+
type JoinCondition = {
|
|
44
|
+
readonly type: "join_on";
|
|
45
|
+
expr: WhereExpr;
|
|
46
|
+
} | {
|
|
47
|
+
readonly type: "join_using";
|
|
48
|
+
columns: string[];
|
|
49
|
+
};
|
|
50
|
+
interface JoinClause {
|
|
51
|
+
readonly type: "join";
|
|
52
|
+
joinType: JoinType;
|
|
53
|
+
table: TableRef;
|
|
54
|
+
condition: JoinCondition | null;
|
|
55
|
+
}
|
|
56
|
+
type SelectFrom = {
|
|
57
|
+
readonly type: "select_from";
|
|
58
|
+
table: TableRef;
|
|
59
|
+
};
|
|
60
|
+
type LimitClause = {
|
|
61
|
+
readonly type: "limit";
|
|
62
|
+
value: number;
|
|
63
|
+
};
|
|
64
|
+
type WhereRoot = {
|
|
65
|
+
readonly type: "where_root";
|
|
66
|
+
inner: WhereExpr;
|
|
67
|
+
};
|
|
68
|
+
type WhereExpr = WhereAnd | WhereOr | WhereNot | WhereComparison | WhereIsNull | WhereIsBool | WhereBetween | WhereIn | WhereLike;
|
|
10
69
|
interface WhereAnd {
|
|
11
|
-
type: "
|
|
70
|
+
readonly type: "where_and";
|
|
12
71
|
left: WhereExpr;
|
|
13
72
|
right: WhereExpr;
|
|
14
73
|
}
|
|
15
74
|
interface WhereOr {
|
|
16
|
-
type: "
|
|
75
|
+
readonly type: "where_or";
|
|
17
76
|
left: WhereExpr;
|
|
18
77
|
right: WhereExpr;
|
|
19
78
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
79
|
+
type ComparisonOperator = "=" | "<>" | "!=" | "<" | ">" | "<=" | ">=";
|
|
80
|
+
interface WhereNot {
|
|
81
|
+
readonly type: "where_not";
|
|
82
|
+
expr: WhereExpr;
|
|
83
|
+
}
|
|
84
|
+
interface WhereIsNull {
|
|
85
|
+
readonly type: "where_is_null";
|
|
86
|
+
not: boolean;
|
|
87
|
+
expr: WhereValue;
|
|
88
|
+
}
|
|
89
|
+
type IsBoolTarget = boolean | "unknown";
|
|
90
|
+
interface WhereIsBool {
|
|
91
|
+
readonly type: "where_is_bool";
|
|
92
|
+
not: boolean;
|
|
93
|
+
expr: WhereValue;
|
|
94
|
+
target: IsBoolTarget;
|
|
95
|
+
}
|
|
96
|
+
interface WhereBetween {
|
|
97
|
+
readonly type: "where_between";
|
|
98
|
+
not: boolean;
|
|
99
|
+
expr: WhereValue;
|
|
100
|
+
low: WhereValue;
|
|
101
|
+
high: WhereValue;
|
|
102
|
+
}
|
|
103
|
+
interface WhereIn {
|
|
104
|
+
readonly type: "where_in";
|
|
105
|
+
not: boolean;
|
|
106
|
+
expr: WhereValue;
|
|
107
|
+
list: WhereValue[];
|
|
108
|
+
}
|
|
109
|
+
type LikeOp = "like";
|
|
110
|
+
interface WhereLike {
|
|
111
|
+
readonly type: "where_like";
|
|
112
|
+
not: boolean;
|
|
113
|
+
op: LikeOp;
|
|
114
|
+
expr: WhereValue;
|
|
115
|
+
pattern: WhereValue;
|
|
116
|
+
}
|
|
117
|
+
type ArithOp = "+" | "-" | "*" | "/" | "%" | "||";
|
|
118
|
+
interface WhereArith {
|
|
119
|
+
readonly type: "where_arith";
|
|
120
|
+
op: ArithOp;
|
|
121
|
+
left: WhereValue;
|
|
122
|
+
right: WhereValue;
|
|
123
|
+
}
|
|
124
|
+
interface WhereUnaryMinus {
|
|
125
|
+
readonly type: "where_unary_minus";
|
|
126
|
+
expr: WhereValue;
|
|
127
|
+
}
|
|
128
|
+
interface CaseWhen {
|
|
129
|
+
condition: WhereValue;
|
|
130
|
+
result: WhereValue;
|
|
131
|
+
}
|
|
132
|
+
interface CaseExpr {
|
|
133
|
+
readonly type: "case_expr";
|
|
134
|
+
subject: WhereValue | null;
|
|
135
|
+
whens: CaseWhen[];
|
|
136
|
+
else: WhereValue | null;
|
|
137
|
+
}
|
|
138
|
+
interface CastExpr {
|
|
139
|
+
readonly type: "cast_expr";
|
|
140
|
+
expr: WhereValue;
|
|
141
|
+
typeName: string;
|
|
142
|
+
}
|
|
143
|
+
type WhereValue = {
|
|
144
|
+
readonly type: "where_value";
|
|
145
|
+
kind: "string";
|
|
24
146
|
value: string;
|
|
147
|
+
} | {
|
|
148
|
+
readonly type: "where_value";
|
|
149
|
+
kind: "integer";
|
|
150
|
+
value: number;
|
|
151
|
+
} | {
|
|
152
|
+
readonly type: "where_value";
|
|
153
|
+
kind: "float";
|
|
154
|
+
value: number;
|
|
155
|
+
} | {
|
|
156
|
+
readonly type: "where_value";
|
|
157
|
+
kind: "bool";
|
|
158
|
+
value: boolean;
|
|
159
|
+
} | {
|
|
160
|
+
readonly type: "where_value";
|
|
161
|
+
kind: "null";
|
|
162
|
+
} | {
|
|
163
|
+
readonly type: "where_value";
|
|
164
|
+
kind: "column_ref";
|
|
165
|
+
ref: ColumnRef;
|
|
166
|
+
} | {
|
|
167
|
+
readonly type: "where_value";
|
|
168
|
+
kind: "func_call";
|
|
169
|
+
func: FuncCall;
|
|
170
|
+
} | WhereArith | WhereUnaryMinus | CaseExpr | CastExpr;
|
|
171
|
+
interface WhereComparison {
|
|
172
|
+
readonly type: "where_comparison";
|
|
173
|
+
operator: ComparisonOperator;
|
|
174
|
+
left: WhereValue;
|
|
175
|
+
right: WhereValue;
|
|
25
176
|
}
|
|
26
|
-
interface
|
|
27
|
-
type: "column_ref";
|
|
177
|
+
interface ColumnRef {
|
|
178
|
+
readonly type: "column_ref";
|
|
179
|
+
schema?: string;
|
|
28
180
|
table?: string;
|
|
29
181
|
name: string;
|
|
30
182
|
}
|
|
31
|
-
|
|
32
|
-
|
|
183
|
+
type FuncCallArg = {
|
|
184
|
+
kind: "wildcard";
|
|
185
|
+
} | {
|
|
186
|
+
kind: "args";
|
|
187
|
+
distinct: boolean;
|
|
188
|
+
args: WhereValue[];
|
|
189
|
+
};
|
|
190
|
+
interface FuncCall {
|
|
191
|
+
readonly type: "func_call";
|
|
192
|
+
name: string;
|
|
193
|
+
args: FuncCallArg;
|
|
194
|
+
}
|
|
195
|
+
interface ColumnExpr {
|
|
196
|
+
readonly type: "column_expr";
|
|
197
|
+
kind: "wildcard" | "qualified_wildcard" | "expr";
|
|
33
198
|
table?: string;
|
|
34
|
-
|
|
35
|
-
|
|
199
|
+
expr?: WhereValue;
|
|
200
|
+
}
|
|
201
|
+
interface Column {
|
|
202
|
+
readonly type: "column";
|
|
203
|
+
expr: ColumnExpr;
|
|
204
|
+
alias?: Alias;
|
|
205
|
+
}
|
|
206
|
+
interface Alias {
|
|
207
|
+
readonly type: "alias";
|
|
208
|
+
name: string;
|
|
36
209
|
}
|
|
37
210
|
interface TableRef {
|
|
38
|
-
type: "
|
|
211
|
+
readonly type: "table_ref";
|
|
39
212
|
schema?: string;
|
|
40
213
|
name: string;
|
|
41
|
-
alias?:
|
|
214
|
+
alias?: Alias;
|
|
42
215
|
}
|
|
43
216
|
//#endregion
|
|
44
217
|
//#region src/output.d.ts
|
|
@@ -48,31 +221,21 @@ declare function outputSql(ast: SelectStatement): string;
|
|
|
48
221
|
declare function parseSql(expr: string): SelectStatement;
|
|
49
222
|
//#endregion
|
|
50
223
|
//#region src/sanitise.d.ts
|
|
51
|
-
|
|
52
|
-
|
|
224
|
+
interface WhereGuard {
|
|
225
|
+
schema?: string;
|
|
226
|
+
table: string;
|
|
227
|
+
col: string;
|
|
228
|
+
value: string | number;
|
|
229
|
+
}
|
|
230
|
+
declare function sanitiseSql(ast: SelectStatement, {
|
|
53
231
|
schema,
|
|
54
232
|
table,
|
|
55
233
|
col,
|
|
56
234
|
value
|
|
57
|
-
}:
|
|
58
|
-
ast: SelectStatement;
|
|
59
|
-
schema: string;
|
|
60
|
-
table: string;
|
|
61
|
-
col: string;
|
|
62
|
-
value: string;
|
|
63
|
-
}): SelectStatement;
|
|
235
|
+
}: WhereGuard): SelectStatement;
|
|
64
236
|
//#endregion
|
|
65
237
|
//#region src/index.d.ts
|
|
66
|
-
declare function sqyrl(expr: string,
|
|
67
|
-
|
|
68
|
-
table,
|
|
69
|
-
column,
|
|
70
|
-
value
|
|
71
|
-
}: {
|
|
72
|
-
schema: string;
|
|
73
|
-
table: string;
|
|
74
|
-
column: string;
|
|
75
|
-
value: string;
|
|
76
|
-
}): string;
|
|
238
|
+
declare function sqyrl(expr: string, whereGuard: WhereGuard): string;
|
|
239
|
+
declare function makeSqyrl(whereGuard: WhereGuard): (expr: string) => string;
|
|
77
240
|
//#endregion
|
|
78
|
-
export { outputSql, parseSql, sanitiseSql, sqyrl };
|
|
241
|
+
export { makeSqyrl, outputSql, parseSql, sanitiseSql, sqyrl };
|