agent-sql 0.3.2 → 0.3.4

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 CHANGED
@@ -19,6 +19,8 @@ agent-sql works by fully parsing the supplied SQL query into an AST and transfor
19
19
  - **`JOIN`s added:** if needed to reach the guard tenant tables (save on tokens).
20
20
  - **No sneaky joins:** no `join secrets on true`. We have your back.
21
21
 
22
+ I plan to support inserts, updates, CTEs, subqueries once I can convincingly make them safe.
23
+
22
24
  ## Quickstart
23
25
 
24
26
  ```bash
@@ -28,7 +30,7 @@ npm install agent-sql
28
30
  ```ts
29
31
  import { agentSql } from "agent-sql";
30
32
 
31
- const sql = agentSql(`SELECT * FROM msg`, "msg.tenant_id", 123);
33
+ const sql = agentSql("SELECT * FROM msg", "msg.tenant_id", 123);
32
34
 
33
35
  console.log(sql);
34
36
  // SELECT *
@@ -39,30 +41,81 @@ console.log(sql);
39
41
 
40
42
  ## Usage
41
43
 
42
- ### Define once, use many times
44
+ ### Define a schema
43
45
 
44
- The simple approach above is enough to get started.
45
- But since no schema is provided, `JOIN`s will be blocked.
46
- A schema can be passed to `agentSql`, but typically you'll want to set it up once and re-use.
46
+ In the simple example above, all `JOIN`s will be blocked.
47
+ For agent-sql to know what joins and tables permit, you need to define a schema.
48
+ Heads up: if you use Drizzle, you can just [use your Drizzle schema](#integration-with-ai-sdk-and-drizzle).
47
49
 
48
50
  ```ts
49
51
  import { createAgentSql, defineSchema } from "agent-sql";
50
- import { tool } from "ai";
51
- import { sql } from "drizzle-orm";
52
- import { db } from "@/db";
53
52
 
54
53
  // Define your schema.
55
54
  // Only the tables listed will be permitted
56
55
  // Joins can only use the FKs defined here
57
56
  const schema = defineSchema({
58
- user: { id: null },
59
- msg: { userId: { ft: "user", fc: "id" } },
57
+ tenant: { id: null },
58
+ msg: { tenant_id: { ft: "tenant", fc: "id" } },
60
59
  });
61
60
 
62
- function makeSqlTool(userId: string) {
61
+ // Use your schema from above
62
+ // Specify 1+ column->value pairs that will be enforced
63
+ const agentSql = createAgentSql(schema, { "tenant.id": 123 });
64
+
65
+ // Now use it
66
+ const sql = agentSql("SELECT * FROM msg");
67
+ ```
68
+
69
+ Outputs:
70
+
71
+ ```sql
72
+ SELECT
73
+ msg.* -- qualify the *
74
+ FROM msg
75
+ INNER JOIN tenant -- add the needed join for the guard
76
+ ON tenant.id = msg.tenant_id -- use the schema to join correctly
77
+ WHERE tenant.id = 123 -- apply the guard
78
+ LIMIT 10000 -- limit the rows
79
+ ```
80
+
81
+ ### Bad stuff is blocked
82
+
83
+ The following query will be blocked (many times over).
84
+
85
+ ```sql
86
+ SELECT
87
+ sneaky_func('./bad_file') -- won't pass whitelist
88
+ FROM secret
89
+ JOIN random -- not an approved table
90
+ ON random.id = secret.id -- not an approved FK pair
91
+ JOIN danger -- disconnected from join graph
92
+ ON true -- not allowed
93
+ WHERE true -- won't trick anyone
94
+ ```
95
+
96
+ ### Integration with AI SDK and Drizzle
97
+
98
+ If you're using Drizzle, you can skip the schema step and use the one you already have!
99
+
100
+ Just pass it through, and `agentSql` will respect your schema.
101
+
102
+ ```ts
103
+ import { tool } from "ai";
104
+ import { sql } from "drizzle-orm";
105
+
106
+ import { createAgentSql } from "agent-sql";
107
+ import { defineSchemaFromDrizzle } from "agent-sql/drizzle";
108
+
109
+ import { db } from "@/db";
110
+ import * as drizzleSchema from "@/db/schema";
111
+
112
+ // No need to re-enter your schema, we'll pull it in from Drizzle
113
+ const schema = defineSchemaFromDrizzle(drizzleSchema);
114
+
115
+ function makeSqlTool(tenantId: string) {
63
116
  // Create a sanitiser function for this tenant
64
117
  // Specify one or more column->value pairs that will be enforced
65
- const agentSql = createAgentSql(schema, { "user.id": userId });
118
+ const agentSql = createAgentSql(schema, { "tenant.id": tenantId });
66
119
 
67
120
  return tool({
68
121
  description: "Run raw SQL against the DB",
@@ -70,34 +123,22 @@ function makeSqlTool(userId: string) {
70
123
  execute: async ({ query }) => {
71
124
  // The LLM can pass any query it likes, we'll sanitise it if possible
72
125
  // and return helpful error messages if not
73
- const sanitised = agentSql(query);
126
+ const sql = agentSql(query);
74
127
  // Now we can throw that straight at the db and be confident it'll only
75
128
  // return data from the specified tenant
76
- return db.execute(sql.raw(sanitised));
129
+ return db.execute(sql.raw(sql));
77
130
  },
78
131
  });
79
132
  }
80
133
  ```
81
134
 
82
- ### It works with Drizzle
83
-
84
- If you're using Drizzle, you can skip the schema step and use the one you already have!
135
+ ### If you don't want your whole Drizzle schema available
85
136
 
86
- Just pass it through, and `agentSql` will respect your schema.
137
+ You can also exclude tables if you don't want agents to see them:
87
138
 
88
139
  ```ts
89
140
  import { defineSchemaFromDrizzle } from "agent-sql/drizzle";
90
- import * as drizzleSchema from "@/db/schema";
91
-
92
- const schema = defineSchemaFromDrizzle(drizzleSchema);
93
141
 
94
- // The rest as before...
95
- const agentSql = createAgentSql(schema, { "user.id": userId });
96
- ```
97
-
98
- You can also exclude tables if you don't want agents to see them:
99
-
100
- ```ts
101
142
  const schema = defineSchemaFromDrizzle(drizzleSchema, {
102
143
  exclude: ["api_keys"],
103
144
  });
package/dist/index.mjs CHANGED
@@ -793,7 +793,10 @@ function checkJoinColumns(ast, schema) {
793
793
  const joinSettings = schema[join.table.name];
794
794
  if (joinSettings === void 0) return Err(new SanitiseError(`Table ${join.table.name} is not allowed`));
795
795
  if (join.condition === null || join.condition.type === "join_using" || join.condition.expr.type !== "where_comparison" || join.condition.expr.operator !== "=" || join.condition.expr.left.type !== "where_value" || join.condition.expr.left.kind !== "column_ref" || join.condition.expr.right.type !== "where_value" || join.condition.expr.right.kind !== "column_ref") return Err(new SanitiseError("Only JOIN ON column_ref = column_ref supported"));
796
- const { joining, foreign } = getJoinTableRef(join.table.name, join.condition.expr.left.ref, join.condition.expr.right.ref);
796
+ const leftRef = join.condition.expr.left.ref;
797
+ const rightRef = join.condition.expr.right.ref;
798
+ if (leftRef.table !== join.table.name && rightRef.table !== join.table.name) return Err(new SanitiseError(`JOIN ${join.table.name} ON clause does not reference ${join.table.name}`));
799
+ const { joining, foreign } = getJoinTableRef(join.table.name, leftRef, rightRef);
797
800
  const joinTableCol = joinSettings[joining.name];
798
801
  if (joinTableCol === void 0) return Err(new SanitiseError(`Tried to join using ${join.table.name}.${joining.name}`));
799
802
  if (joinTableCol === null) {
@@ -7448,7 +7451,10 @@ semantics.addOperation("toAST()", {
7448
7451
  });
7449
7452
  function parseSql(expr) {
7450
7453
  const matchResult = result.match(expr);
7451
- if (matchResult.failed()) return Err(new ParseError(matchResult.message));
7454
+ if (matchResult.failed()) {
7455
+ const [message] = matchResult.message.split("\nExpected");
7456
+ return Err(new ParseError(`Got invalid SQL. Some language features are deliberately disabled. Re-write your query without them.\n${message}`));
7457
+ }
7452
7458
  try {
7453
7459
  return Ok(semantics(matchResult).toAST());
7454
7460
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sql",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "keywords": [
6
6
  "agent",
@@ -34,6 +34,7 @@
34
34
  "build": "vp pack",
35
35
  "dev": "vp pack --watch",
36
36
  "test": "vp test",
37
+ "coverage": "vp test --coverage",
37
38
  "check": "vp check",
38
39
  "prepublishOnly": "vp run build",
39
40
  "ohm": "ohm generateBundles --withTypes --esm 'src/sql.ohm'",
@@ -48,6 +49,7 @@
48
49
  "@types/node": "^25.5.0",
49
50
  "@types/pg": "^8.18.0",
50
51
  "@typescript/native-preview": "7.0.0-dev.20260316.1",
52
+ "@vitest/coverage-v8": "^4.1.0",
51
53
  "bumpp": "^11.0.1",
52
54
  "drizzle-orm": "^0.45.1",
53
55
  "pg": "^8.20.0",