Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions chadscript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,11 @@ declare namespace crypto {

declare namespace sqlite {
function open(path: string): any;
function exec(db: any, sql: string): void;
function get(db: any, sql: string): string;
function all(db: any, sql: string): string[];
function exec(db: any, sql: string, params?: any[]): void;
function get(db: any, sql: string, params?: any[]): string;
function getRow<T = any>(db: any, sql: string, params?: any[]): T;
function all(db: any, sql: string, params?: any[]): string[];
function query<T = any>(db: any, sql: string, params?: any[]): T[];
function close(db: any): void;
}

Expand Down
108 changes: 80 additions & 28 deletions docs/stdlib/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,72 @@ const db = sqlite.open("myapp.db");
const memdb = sqlite.open(":memory:");
```

## `sqlite.exec(db, sql)`
## `sqlite.exec(db, sql, params?)`

Execute DDL or DML statements (CREATE, INSERT, UPDATE, DELETE).
Execute DDL or DML statements (CREATE, INSERT, UPDATE, DELETE). Use `?` placeholders and pass values as a third argument to avoid SQL injection.

```typescript
sqlite.exec(db, "CREATE TABLE users (id INTEGER, name TEXT)");
sqlite.exec(db, "INSERT INTO users VALUES (1, 'Alice')");
sqlite.exec(db, "CREATE TABLE users (id INTEGER, name TEXT, age INTEGER)");
sqlite.exec(db, "INSERT INTO users VALUES (?, ?, ?)", [1, "Alice", 30]);
```

## `sqlite.get(db, sql)`
## `sqlite.get(db, sql, params?)`

Execute a query and return the first row as a string. Multi-column results are pipe-separated.
Execute a query and return the first row as a string. Single-column results return the value directly; multi-column results are pipe-separated. For typed multi-column access, prefer `sqlite.getRow()`.

```typescript
const name = sqlite.get(db, "SELECT name FROM users WHERE id = 1");
const name = sqlite.get(db, "SELECT name FROM users WHERE id = ?", [1]);
// "Alice"
```

## `sqlite.getRow<T>(db, sql, params?)`

const row = sqlite.get(db, "SELECT id, name FROM users WHERE id = 1");
// "1|Alice"
Execute a query and return the first row as a typed object, or `null` if no row matches. Fields are accessed by position via type assertion.

```typescript
interface User {
id: string;
name: string;
age: string;
}

const user = sqlite.getRow<User>(db, "SELECT id, name, age FROM users WHERE id = ?", [1]);
if (user !== null) {
console.log(user.name); // "Alice"
}
```

## `sqlite.all(db, sql)`
## `sqlite.all(db, sql, params?)`

Execute a query and return all rows as a string array. Multi-column results are pipe-separated.
Execute a query and return all rows as a string array. Single-column results return values directly. For typed multi-column access, prefer `sqlite.query()`.

```typescript
const names = sqlite.all(db, "SELECT name FROM users ORDER BY id");
// ["Alice", "Bob"]
const names = sqlite.all(db, "SELECT name FROM users WHERE age > ?", [25]);
// ["Alice", "Charlie"]
```

## `sqlite.query<T>(db, sql, params?)`

Execute a query and return all rows as a typed object array. This is the recommended API for multi-column queries.

const rows = sqlite.all(db, "SELECT id, name FROM users ORDER BY id");
// ["1|Alice", "2|Bob"]
const parts = rows[0].split('|');
// parts[0] = "1", parts[1] = "Alice"
```typescript
interface User {
id: string;
name: string;
age: string;
}

const users = sqlite.query<User>(db, "SELECT id, name, age FROM users ORDER BY id");
for (const user of users) {
console.log(user.name + " age " + user.age);
}

// With parameters:
const adults = sqlite.query<User>(
db,
"SELECT id, name, age FROM users WHERE age >= ?",
["18"]
);
```

## `sqlite.close(db)`
Expand All @@ -58,29 +91,48 @@ sqlite.close(db);

```typescript
const db = sqlite.open(":memory:");
sqlite.exec(db, "CREATE TABLE users (id INTEGER, name TEXT)");
sqlite.exec(db, "INSERT INTO users VALUES (1, 'Alice')");
sqlite.exec(db, "INSERT INTO users VALUES (2, 'Bob')");
sqlite.exec(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
sqlite.exec(db, "INSERT INTO users (name, age) VALUES (?, ?)", ["Alice", 30]);
sqlite.exec(db, "INSERT INTO users (name, age) VALUES (?, ?)", ["Bob", 25]);

interface User {
id: string;
name: string;
age: string;
}

const name = sqlite.get(db, "SELECT name FROM users WHERE id = 1");
console.log(name); // "Alice"
const users = sqlite.query<User>(db, "SELECT id, name, age FROM users ORDER BY name");
console.log(users.length); // 2
console.log(users[0].name); // "Alice"

const names = sqlite.all(db, "SELECT name FROM users ORDER BY id");
console.log(names.length); // 2
const alice = sqlite.getRow<User>(db, "SELECT id, name, age FROM users WHERE name = ?", ["Alice"]);
if (alice !== null) {
console.log(alice.age); // "30"
}

sqlite.close(db);
```

::: tip
Multi-column queries return pipe-separated values. Use `.split('|')` to access individual columns.
:::
## Parameterized Queries

Always use `?` placeholders with a params array instead of string interpolation. This prevents SQL injection and is the only safe approach when handling user input.

```typescript
// safe
const row = sqlite.getRow(db, "SELECT * FROM users WHERE name = ?", [userInput]);

// unsafe — never do this with user input
const row2 = sqlite.get(db, "SELECT * FROM users WHERE name = '" + userInput + "'");
```

## Native Implementation

| API | Maps to |
|-----|---------|
| `sqlite.open()` | `sqlite3_open()` |
| `sqlite.exec()` | `sqlite3_exec()` |
| `sqlite.exec()` | `sqlite3_prepare_v2()` + `sqlite3_bind_text()` + `sqlite3_step()` |
| `sqlite.get()` | `sqlite3_prepare_v2()` + `sqlite3_step()` |
| `sqlite.getRow()` | `sqlite3_prepare_v2()` + `sqlite3_step()` → field struct |
| `sqlite.all()` | `sqlite3_prepare_v2()` + `sqlite3_step()` loop |
| `sqlite.query()` | `sqlite3_prepare_v2()` + `sqlite3_step()` loop → field structs |
| `sqlite.close()` | `sqlite3_close()` |
24 changes: 19 additions & 5 deletions examples/query.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
// SQLite Query - demonstrates embedded SQLite database operations

interface User {
id: string;
name: string;
role: string;
}

console.log("SQLite Demo");
console.log(" database: :memory:");
console.log("");

const db = sqlite.open(":memory:");
sqlite.exec(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, role TEXT)");
sqlite.exec(db, "INSERT INTO users (name, role) VALUES ('Alice', 'admin')");
sqlite.exec(db, "INSERT INTO users (name, role) VALUES ('Bob', 'developer')");
sqlite.exec(db, "INSERT INTO users (name, role) VALUES ('Charlie', 'designer')");
sqlite.exec(db, "INSERT INTO users (name, role) VALUES (?, ?)", ["Alice", "admin"]);
sqlite.exec(db, "INSERT INTO users (name, role) VALUES (?, ?)", ["Bob", "developer"]);
sqlite.exec(db, "INSERT INTO users (name, role) VALUES (?, ?)", ["Charlie", "designer"]);

console.log("Inserted 3 users. Querying...");
console.log("");

const rows = sqlite.all(db, "SELECT * FROM users");
const rows: User[] = sqlite.query<User>(db, "SELECT id, name, role FROM users ORDER BY name");
console.log("Found " + rows.length + " rows:");
for (let i = 0; i < rows.length; i++) {
console.log(" " + rows[i]);
console.log(" " + rows[i].id + " | " + rows[i].name + " | " + rows[i].role);
}

console.log("");
const alice: User = sqlite.getRow<User>(db, "SELECT id, name, role FROM users WHERE name = ?", [
"Alice",
]);
if (alice !== null) {
console.log("Alice's role: " + alice.role);
}

sqlite.close(db);
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/expressions/method-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,8 @@ export class MethodCallGenerator {
return this.ctx.sqliteGen.generateExec(expr, params);
} else if (method === "get") {
return this.ctx.sqliteGen.generateGet(expr, params);
} else if (method === "getRow") {
return this.ctx.sqliteGen.generateGetRow(expr, params);
} else if (method === "all") {
return this.ctx.sqliteGen.generateAll(expr, params);
} else if (method === "query") {
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/infrastructure/generator-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export interface ISqliteGenerator {
generateOpen(expr: MethodCallNode, params: string[]): string;
generateExec(expr: MethodCallNode, params: string[]): string;
generateGet(expr: MethodCallNode, params: string[]): string;
generateGetRow(expr: MethodCallNode, params: string[]): string;
generateAll(expr: MethodCallNode, params: string[]): string;
generateQuery(expr: MethodCallNode, params: string[]): string;
generateClose(expr: MethodCallNode, params: string[]): string;
Expand Down Expand Up @@ -1919,6 +1920,7 @@ export class MockGeneratorContext implements IGeneratorContext {
generateOpen: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_open",
generateExec: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_exec",
generateGet: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_get",
generateGetRow: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_get_row",
generateAll: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_all",
generateQuery: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_query",
generateClose: (_expr: MethodCallNode, _params: string[]): string => "%mock_sqlite_close",
Expand Down
7 changes: 6 additions & 1 deletion src/codegen/infrastructure/interface-allocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,12 @@ export class InterfaceAllocator {
if (innerType.startsWith("[")) return null;
return strippedDeclaredType;
}
if (!stmt.value || (stmt.value.type !== "variable" && stmt.value.type !== "object"))
if (
!stmt.value ||
(stmt.value.type !== "variable" &&
stmt.value.type !== "object" &&
stmt.value.type !== "method_call")
)
return null;
const interfaceDefResult2 = this.getInterface(stmt.declaredType);
if (!interfaceDefResult2) return null;
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,8 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
finalParts.push(this.sqliteGen.generateSqliteRowToStructHelper());
finalParts.push(this.sqliteGen.generateSqliteQueryHelper());
finalParts.push(this.sqliteGen.generateSqliteQueryWithParamsHelper());
finalParts.push(this.sqliteGen.generateSqliteGetRowHelper());
finalParts.push(this.sqliteGen.generateSqliteGetRowWithParamsHelper());
}

if (this.usesStringBuilder) {
Expand Down
86 changes: 85 additions & 1 deletion src/codegen/stdlib/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class SqliteGenerator {
if (exprObjBase.type !== "variable") return false;
const varNode = expr.object as { type: string; name: string };
if (varNode.name !== "sqlite") return false;
const supported = ["open", "exec", "get", "all", "query", "close"];
const supported = ["open", "exec", "get", "getRow", "all", "query", "close"];
return supported.indexOf(expr.method) !== -1;
}

Expand Down Expand Up @@ -90,6 +90,32 @@ export class SqliteGenerator {
return result;
}

// sqlite.getRow() — returns a single typed struct (i8*) instead of a pipe-delimited string
generateGetRow(expr: MethodCallNode, params: string[]): string {
if (expr.args.length < 2) {
return this.ctx.emitError("sqlite.getRow() requires 2 arguments (db, sql)", expr.loc);
}

const dbPtr = this.ctx.generateExpression(expr.args[0], params);
const sqlPtr = this.ctx.generateExpression(expr.args[1], params);

if (expr.args.length >= 3) {
const paramsArr = this.buildParamsArray(expr.args[2], params);
const result = this.ctx.nextTemp();
this.ctx.emit(
`${result} = call i8* @__sqlite_get_row_params(i8* ${dbPtr}, i8* ${sqlPtr}, %StringArray* ${paramsArr})`,
);
this.ctx.setVariableType(result, "i8*");
return result;
}

const result = this.ctx.nextTemp();
this.ctx.emit(`${result} = call i8* @__sqlite_get_row(i8* ${dbPtr}, i8* ${sqlPtr})`);
this.ctx.setVariableType(result, "i8*");

return result;
}

// sqlite.query() — returns %ObjectArray* of typed structs instead of pipe-delimited strings
generateQuery(expr: MethodCallNode, params: string[]): string {
if (expr.args.length < 2) {
Expand Down Expand Up @@ -641,6 +667,64 @@ export class SqliteGenerator {
return ir;
}

// Returns null for no row, otherwise a struct of i8* fields (same layout as __sqlite_row_to_struct).
generateSqliteGetRowHelper(): string {
let ir = "";
ir += "define i8* @__sqlite_get_row(i8* %db, i8* %sql) {\n";
ir += "entry:\n";
ir += " %sql_len = call i64 @strlen(i8* %sql)\n";
ir += " %sql_len_i32 = trunc i64 %sql_len to i32\n";
ir += " %stmt_ptr_raw = call i8* @GC_malloc(i64 8)\n";
ir += " %stmt_ptr = bitcast i8* %stmt_ptr_raw to i8**\n";
ir +=
" %rc = call i32 @sqlite3_prepare_v2(i8* %db, i8* %sql, i32 %sql_len_i32, i8** %stmt_ptr, i8** null)\n";
ir += " %stmt = load i8*, i8** %stmt_ptr\n";
ir += " %step_rc = call i32 @sqlite3_step(i8* %stmt)\n";
ir += " %is_row = icmp eq i32 %step_rc, 100\n";
ir += " br i1 %is_row, label %has_row, label %no_row\n";
ir += "\n";
ir += "has_row:\n";
ir += " %col_count = call i32 @sqlite3_column_count(i8* %stmt)\n";
ir += " %result = call i8* @__sqlite_row_to_struct(i8* %stmt, i32 %col_count)\n";
ir += " call i32 @sqlite3_finalize(i8* %stmt)\n";
ir += " ret i8* %result\n";
ir += "\n";
ir += "no_row:\n";
ir += " call i32 @sqlite3_finalize(i8* %stmt)\n";
ir += " ret i8* null\n";
ir += "}\n\n";
return ir;
}

generateSqliteGetRowWithParamsHelper(): string {
let ir = "";
ir += "define i8* @__sqlite_get_row_params(i8* %db, i8* %sql, %StringArray* %params) {\n";
ir += "entry:\n";
ir += " %sql_len = call i64 @strlen(i8* %sql)\n";
ir += " %sql_len_i32 = trunc i64 %sql_len to i32\n";
ir += " %stmt_ptr_raw = call i8* @GC_malloc(i64 8)\n";
ir += " %stmt_ptr = bitcast i8* %stmt_ptr_raw to i8**\n";
ir +=
" %rc = call i32 @sqlite3_prepare_v2(i8* %db, i8* %sql, i32 %sql_len_i32, i8** %stmt_ptr, i8** null)\n";
ir += " %stmt = load i8*, i8** %stmt_ptr\n";
ir += " call void @__sqlite_bind_params(i8* %stmt, %StringArray* %params)\n";
ir += " %step_rc = call i32 @sqlite3_step(i8* %stmt)\n";
ir += " %is_row = icmp eq i32 %step_rc, 100\n";
ir += " br i1 %is_row, label %has_row, label %no_row\n";
ir += "\n";
ir += "has_row:\n";
ir += " %col_count_grp = call i32 @sqlite3_column_count(i8* %stmt)\n";
ir += " %result = call i8* @__sqlite_row_to_struct(i8* %stmt, i32 %col_count_grp)\n";
ir += " call i32 @sqlite3_finalize(i8* %stmt)\n";
ir += " ret i8* %result\n";
ir += "\n";
ir += "no_row:\n";
ir += " call i32 @sqlite3_finalize(i8* %stmt)\n";
ir += " ret i8* null\n";
ir += "}\n\n";
return ir;
}

generateSqliteAllWithParamsHelper(): string {
let ir = "";
ir += "define %StringArray* @__sqlite_all_params(i8* %db, i8* %sql, %StringArray* %params) {\n";
Expand Down
Loading
Loading