Skip to content

Support sqlite#189

Merged
husamql3 merged 2 commits into
stagefrom
support-sqlite
May 9, 2026
Merged

Support sqlite#189
husamql3 merged 2 commits into
stagefrom
support-sqlite

Conversation

@husamql3

@husamql3 husamql3 commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added SQLite database support as a new connection option alongside PostgreSQL, MySQL, MongoDB, and SQL Server.
    • Added SQLite wordmark to the landing page in the "Works with your stack" section.
    • New database initialization script enables quick SQLite setup.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds complete SQLite database support to db-studio by implementing a new adapter, query builders, type mappers, and database manager integration. The changes span server backend logic, shared type definitions, initialization scripts, and frontend UI updates to advertise SQLite as a supported database alongside PostgreSQL, MySQL, MSSQL, and MongoDB.

Changes

SQLite Database Support

Layer / File(s) Summary
Type Definitions & Contracts
packages/shared/src/types/database.types.ts, packages/shared/src/types/column.type.ts
DATABASE_TYPES enum now includes "sqlite"; added mapSqliteToDataType and standardizeSqliteDataTypeLabel for SQLite type affinity mapping.
Query Building Helpers
packages/server/src/adapters/sqlite/sqlite.query-builder.ts
Three helper functions construct WHERE/ORDER BY/cursor-based row-value comparison SQL clauses with proper placeholders and parameter arrays.
SQLite Adapter Implementation
packages/server/src/adapters/sqlite/sqlite.adapter.ts
Comprehensive SqliteAdapter extending BaseAdapter with database/table introspection, DDL operations, record CRUD with FK constraint detection, pagination/cursors using primary keys or rowid fallback, and raw query execution.
Database Connection Management
packages/server/src/db-manager.ts
DatabaseManager gains SQLite support: lazy-initializes a singleton connection via better-sqlite3, sets WAL and foreign-key pragmas, detects sqlite:// URLs, and exports getSqliteDb() for adapter use.
Server Adapter Registration & Exports
packages/server/src/adapters/register.ts, packages/server/src/adapters/connections.ts, packages/server/src/cmd/get-db-url.ts
SqliteAdapter is registered at startup; getSqliteDb() is exported as a connection helper; sqlite://... URL format is validated in manual database URL entry.
Dependencies & Initialization
packages/server/package.json, package.json, scripts/init-db-sqlite.sh
Added better-sqlite3@^12.9.0 runtime dependency; init-db:sqlite npm script invokes a Bash initialization script that creates the database, enables WAL/FK pragmas, creates tables with relationships, seeds data, and updates .env.
UI & Documentation
www/src/components/ui/svgs/sqliteWordmark.tsx, www/src/routes/(main)/_pathlessLayout/index.tsx, CLAUDE.md
New SQLite SVG wordmark component integrated into the landing page's "Works with your stack" slider; CLAUDE.md added for Claude Code guidance.
Tests
packages/server/tests/adapters/sqlite.adapter.test.ts, packages/server/tests/adapters/sqlite.query-builder.test.ts
Comprehensive test suites covering adapter CRUD, pagination, FK constraint handling, type mapping, query builders, cursor logic, and error-case validation with mocked SQLite.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • husamql3/db-studio#148: Modifies shared DATABASE_TYPES enum and adapter/db-manager infrastructure for multi-database support.
  • husamql3/db-studio#137: Adds a new database type via DATABASE_TYPES enum and db-manager connection integration.
  • husamql3/db-studio#129: Adds another new database type (MSSQL) with similar adapter and type-mapping patterns.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support sqlite' directly and specifically describes the main objective of the pull request, which adds comprehensive SQLite database support throughout the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch support-sqlite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown

Bundle Size

packages/server/dist692K

File breakdown
152K	packages/server/dist/index.js
536K	packages/server/dist/index.js.map

packages/web/dist17M

File breakdown
4.0K	packages/web/dist/assets/_pathlessLayout-DVMjmY8c.js
56K	packages/web/dist/assets/_pathlessLayout-q8rvg9L4.js
4.0K	packages/web/dist/assets/_queryId-DXGCwoqB.js
4.0K	packages/web/dist/assets/_table-BTe1rkPn.js
4.0K	packages/web/dist/assets/_table-CaGOYMVS.js
28K	packages/web/dist/assets/bundle-mjs-B5mORsDs.js
444K	packages/web/dist/assets/chunk-BO2N2NFS-LCUq0Gxt.js
8.0K	packages/web/dist/assets/code-editor-Cp9GUV89.js
120K	packages/web/dist/assets/codicon-ngg6Pgfi.ttf
1.3M	packages/web/dist/assets/css.worker-CCg9Iwze.js
4.0K	packages/web/dist/assets/highlighted-body-OFNGDK62-DZCoRAeP.js
860K	packages/web/dist/assets/html.worker-BBXyX7jd.js
20K	packages/web/dist/assets/icons-CWGGEoI2.js
548K	packages/web/dist/assets/index-DiaZxCvJ.js
124K	packages/web/dist/assets/index-rzbA8gTj.css
4.0K	packages/web/dist/assets/indexes-1639QCZX.js
12K	packages/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2
12K	packages/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2
16K	packages/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2
40K	packages/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2
8.0K	packages/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2
516K	packages/web/dist/assets/json.worker-D3YLuqJL.js
4.0K	packages/web/dist/assets/logs-b9GFrvxu.js
4.0K	packages/web/dist/assets/mermaid-GHXKKRXX-DNPzzFwN.js
4.1M	packages/web/dist/assets/monaco-editor-B6TxHuqP.js
140K	packages/web/dist/assets/monaco-editor-DVSAlpqy.css
4.0K	packages/web/dist/assets/queries.store-BMB2DqwI.js
12K	packages/web/dist/assets/query-runner-BE4ZrAx8.js
148K	packages/web/dist/assets/radix-ui-DdxcFOeB.js
176K	packages/web/dist/assets/react-dom-B7tPHdJv.js
4.0K	packages/web/dist/assets/rolldown-runtime-Bnw7wDfq.js
4.0K	packages/web/dist/assets/runner-D7ZYEZ0b.js
4.0K	packages/web/dist/assets/schema-BAXnnZXT.js
4.0K	packages/web/dist/assets/table-CnHF7LzP.js
232K	packages/web/dist/assets/tanstack-edM6nHGa.js
7.8M	packages/web/dist/assets/ts.worker-EBDMY3hW.js
4.0K	packages/web/dist/assets/visualizer-wABFdtgu.js
104K	packages/web/dist/image.png
4.0K	packages/web/dist/index.html

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/src/adapters/sqlite/sqlite.adapter.ts`:
- Around line 352-413: The public adapter methods getDatabasesList,
getCurrentDatabase, getDatabaseConnectionInfo, and getTablesList must be wrapped
in try/catch blocks so any errors are rethrown via the adapter helper; update
each of these methods to enclose the current body in try { ... } catch (e) {
throw this.wrapError(e); } (and do the same for the other adapter overrides
mentioned in the later block around lines 495–557) so better-sqlite3/filesystem
failures are converted into the adapter’s 503-mapped error using this.wrapError.
- Around line 241-284: The ORDER BY must include the PK tie-breaker columns from
cursorColumns to ensure stable ordering; update the logic that sets
effectiveSortClause (which currently starts from sortClause built by
buildSortClause) to append any cursorColumns not already present in the
sortClause with the correct direction (use effectiveSortDirection for
tie-breaker direction when direction is "asc", and invert appropriately when
direction is "desc" or when you applied the TEMP_DESC swap), ensuring you
preserve existing explicit sort entries from buildSortClause and only add
missing PK columns (quoted the same way as other columns) so the final
effectiveSortClause deterministically orders by (requested sort..., pk1,
pk2...).
- Around line 758-765: addRecord() and bulkInsertRecords() currently bind
object/array values raw while updateRecords() stringifies them, causing
inconsistent storage for JSON/array columns; update the insert paths used in
addRecord(), bulkInsertRecords() (the code building values, colNames,
placeholders and calling sqliteDb.prepare(...).run(...)) to mirror
updateRecords() by mapping over Object.values(data) and JSON.stringify any value
that is an object or array before passing to .run(...), ensuring consistent
serialization for object/array types across inserts and updates.

In `@packages/server/src/adapters/sqlite/sqlite.query-builder.ts`:
- Around line 13-14: The code interpolates raw identifiers (e.g.,
filter.columnName and entries in sortColumns) into quoted SQL identifiers (see
the line that sets col = `"${filter.columnName}"` and the other occurrences
around sort/ cursor construction), which allows injection if the value contains
quotes; fix by validating or escaping identifiers before interpolation: either
whitelist/validate each identifier against a safe pattern like
/^[A-Za-z_][A-Za-z0-9_]*$/ (reject or throw on invalid names) or escape double
quotes by replacing " with "" per SQLite identifier quoting rules, then use the
sanitized identifier in place of filter.columnName and in all uses of
sortColumns and cursor column interpolation. Ensure the same sanitization
utility is used consistently in the functions that build filters, sorts, and
cursor clauses (the places referenced around the col assignment and the
sorts/cursors).

In `@packages/server/src/db-manager.ts`:
- Around line 165-170: The SQLite file handle is only closed by closeSqliteDb()
when closeAll() runs, but closePool() and closePoolByDatabase() skip it and can
leave the handle open; update closePool() and closePoolByDatabase() to call
closeSqliteDb() (or perform the same sqliteDb?.close() and sqliteDb = null
logic) whenever the pool being closed corresponds to the SQLite database (match
by the same database identifier used in those methods), ensuring the sqliteDb is
closed and nulled in selective-close flows as well; keep closeSqliteDb() as the
single implementation to avoid duplication and call it from
closePool()/closePoolByDatabase().

In `@packages/shared/src/types/column.type.ts`:
- Around line 630-639: The type-normalization logic misclassifies parameterized
types like "bigint(20)" or "smallint(6)" because the generic includes("int")
branch runs first; update the comparisons to match whole words or prefixes
before the generic match (e.g., use normalized.startsWith("bigint") /
/^bigint(\(|$)/, and similarly for smallint, tinyint, mediumint) and do the same
for float/double (startsWith or regex to catch "float(8)" / "double
precision(10)"); ensure these checks still return the correct
StandardizedDataType values and keep the generic normalized.includes("int")
fallback last (apply the same change to the corresponding float/double block
referenced in the diff).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 97c0aa4a-25bb-4a54-8971-47f5182d58e8

📥 Commits

Reviewing files that changed from the base of the PR and between 658db5a and bde8716.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • CLAUDE.md
  • db/dbstudio.sqlite
  • db/dbstudio.sqlite-shm
  • db/dbstudio.sqlite-wal
  • package.json
  • packages/server/package.json
  • packages/server/src/adapters/connections.ts
  • packages/server/src/adapters/register.ts
  • packages/server/src/adapters/sqlite/sqlite.adapter.ts
  • packages/server/src/adapters/sqlite/sqlite.query-builder.ts
  • packages/server/src/cmd/get-db-url.ts
  • packages/server/src/db-manager.ts
  • packages/server/tests/adapters/sqlite.adapter.test.ts
  • packages/server/tests/adapters/sqlite.query-builder.test.ts
  • packages/shared/src/types/column.type.ts
  • packages/shared/src/types/database.types.ts
  • scripts/init-db-sqlite.sh
  • www/src/components/ui/svgs/sqliteWordmark.tsx
  • www/src/routes/(main)/_pathlessLayout/index.tsx

Comment on lines +241 to +284
const cursorColumns = [
...sortColumns,
...pkColumns.filter((pk) => !sortColumns.includes(pk)),
];
const useRowid = cursorColumns.length === 0;
if (useRowid) cursorColumns.push("rowid");

const { clause: filterWhere, values: filterValues } = buildWhereClause(filters);

let cursorWhere = "";
let cursorValues: unknown[] = [];
if (cursor) {
const cursorData = this.decodeCursor(cursor);
if (cursorData) {
const res = buildCursorWhereClause(cursorData, direction, effectiveSortDirection);
cursorWhere = res.clause;
cursorValues = res.values;
}
}

let combinedWhere = "";
if (filterWhere && cursorWhere) {
combinedWhere = `WHERE ${filterWhere.replace(/^WHERE\s+/i, "")} AND ${cursorWhere}`;
} else if (filterWhere) {
combinedWhere = filterWhere;
} else if (cursorWhere) {
combinedWhere = `WHERE ${cursorWhere}`;
}

const sortClause = buildSortClause(Array.isArray(sort) ? sort : sort, order);
let effectiveSortClause = sortClause;

if (direction === "desc") {
if (sortClause) {
effectiveSortClause = sortClause
.replace(/\bASC\b/gi, "TEMP_DESC")
.replace(/\bDESC\b/gi, "ASC")
.replace(/TEMP_DESC/g, "DESC");
} else {
effectiveSortClause = `ORDER BY ${cursorColumns.map((col) => `"${col}" ${effectiveSortDirection === "asc" ? "DESC" : "ASC"}`).join(", ")}`;
}
} else if (!sortClause) {
effectiveSortClause = `ORDER BY ${cursorColumns.map((col) => `"${col}" ${effectiveSortDirection.toUpperCase()}`).join(", ")}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Append the PK tie-breakers to ORDER BY too.

cursorColumns correctly adds primary-key columns for stable cursors, but effectiveSortClause still orders only by the requested sort. If that sort is non-unique, the query order inside each peer group is undefined while the cursor predicate compares (sort, pk), so rows can be skipped or duplicated across pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/adapters/sqlite/sqlite.adapter.ts` around lines 241 -
284, The ORDER BY must include the PK tie-breaker columns from cursorColumns to
ensure stable ordering; update the logic that sets effectiveSortClause (which
currently starts from sortClause built by buildSortClause) to append any
cursorColumns not already present in the sortClause with the correct direction
(use effectiveSortDirection for tie-breaker direction when direction is "asc",
and invert appropriately when direction is "desc" or when you applied the
TEMP_DESC swap), ensuring you preserve existing explicit sort entries from
buildSortClause and only add missing PK columns (quoted the same way as other
columns) so the final effectiveSortClause deterministically orders by (requested
sort..., pk1, pk2...).

Comment on lines +352 to +413
async getDatabasesList(): Promise<DatabaseInfoSchemaType[]> {
const sqliteDb = getSqliteDb();
const rows = sqliteDb.prepare("PRAGMA database_list").all() as Array<{
seq: number;
name: string;
file: string;
}>;

if (!rows.length)
throw new HTTPException(500, { message: "No databases returned from SQLite" });

return rows.map((row) => ({
name: row.name,
size: this.getFileSize(row.file),
owner: "",
encoding: "UTF-8",
}));
}

async getCurrentDatabase(): Promise<DatabaseSchemaType> {
const sqliteDb = getSqliteDb();
const rows = sqliteDb.prepare("PRAGMA database_list").all() as Array<{ name: string }>;
const main = rows.find((r) => r.name === "main");
return { db: main?.name ?? "main" };
}

async getDatabaseConnectionInfo(): Promise<ConnectionInfoSchemaType> {
const sqliteDb = getSqliteDb();
const versionRow = sqliteDb.prepare("SELECT sqlite_version() as version").get() as {
version: string;
};

return {
host: null,
port: null,
user: "",
database: "main",
version: `SQLite ${versionRow.version}`,
active_connections: 1,
max_connections: 1,
};
}

// =========================================================
// IDbAdapter — Tables
// =========================================================

async getTablesList(_db: DatabaseSchemaType["db"]): Promise<TableInfoSchemaType[]> {
const sqliteDb = getSqliteDb();
const tables = sqliteDb
.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`,
)
.all() as Array<{ name: string }>;

return tables.map((t) => {
const countRow = sqliteDb.prepare(`SELECT COUNT(*) as count FROM "${t.name}"`).get() as {
count: number;
};
return { tableName: t.name, rowCount: countRow?.count ?? 0 };
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Wrap these public overrides with throw this.wrapError(e) as well.

These methods call better-sqlite3/filesystem APIs directly without a top-level try/catch, so connection failures bypass the adapter’s normal 503 mapping and surface as generic errors instead.

As per coding guidelines, "packages/server/src/adapters/**/*.adapter.ts: Database adapter template method overrides must wrap their body in try/catch and call throw this.wrapError(e) to surface connection errors as 503."

Also applies to: 495-557

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/adapters/sqlite/sqlite.adapter.ts` around lines 352 -
413, The public adapter methods getDatabasesList, getCurrentDatabase,
getDatabaseConnectionInfo, and getTablesList must be wrapped in try/catch blocks
so any errors are rethrown via the adapter helper; update each of these methods
to enclose the current body in try { ... } catch (e) { throw this.wrapError(e);
} (and do the same for the other adapter overrides mentioned in the later block
around lines 495–557) so better-sqlite3/filesystem failures are converted into
the adapter’s 503-mapped error using this.wrapError.

Comment on lines +645 to +691
const newColDefs = colInfo.map((col) => {
const isTarget = col.name === columnName;
const type = isTarget ? columnType : col.type;
const nullable = isTarget ? isNullable : col.notnull === 0;
const defVal = isTarget ? defaultValue?.trim() || null : col.dflt_value;
const isPk = col.pk > 0;

let def = `"${col.name}" ${type}`;
if (!hasCompositePk && isPk) def += " PRIMARY KEY";
if (!isPk && !nullable) def += " NOT NULL";
if (defVal) def += ` DEFAULT ${defVal}`;
return def;
});

if (hasCompositePk) {
newColDefs.push(`PRIMARY KEY (${pkCols.map((c) => `"${c.name}"`).join(", ")})`);
}

const fks = sqliteDb.prepare(`PRAGMA foreign_key_list("${tableName}")`).all() as FkRow[];
const fksByGroup = new Map<number, FkRow[]>();
for (const fk of fks) {
const arr = fksByGroup.get(fk.id) ?? [];
arr.push(fk);
fksByGroup.set(fk.id, arr);
}
const fkDefs = Array.from(fksByGroup.values()).map((group) => {
const from = group.map((f) => `"${f.from}"`).join(", ");
const to = group.map((f) => `"${f.to}"`).join(", ");
const { table, on_update, on_delete } = group[0];
return `FOREIGN KEY (${from}) REFERENCES "${table}" (${to}) ON UPDATE ${on_update} ON DELETE ${on_delete}`;
});

const colNames = colInfo.map((c) => `"${c.name}"`).join(", ");
const tempName = `${tableName}_alter_${Date.now()}`;

sqliteDb.pragma("foreign_keys = OFF");
const doAlter = sqliteDb.transaction(() => {
sqliteDb
.prepare(`CREATE TABLE "${tempName}" (${[...newColDefs, ...fkDefs].join(", ")})`)
.run();
sqliteDb
.prepare(
`INSERT INTO "${tempName}" (${colNames}) SELECT ${colNames} FROM "${tableName}"`,
)
.run();
sqliteDb.prepare(`DROP TABLE "${tableName}"`).run();
sqliteDb.prepare(`ALTER TABLE "${tempName}" RENAME TO "${tableName}"`).run();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Recreating the table here drops secondary schema objects.

This rebuild only preserves column definitions, PKs, defaults, and FKs from PRAGMA table_info/foreign_key_list. Existing UNIQUE, CHECK, collations, generated columns, indexes, and triggers are not recreated, so altering one column can silently weaken constraints and query plans on unrelated columns.

Comment on lines +758 to +765
const values = Object.values(data);
const colNames = columns.map((c) => `"${c}"`).join(", ");
const placeholders = columns.map(() => "?").join(", ");

try {
const result = sqliteDb
.prepare(`INSERT INTO "${tableName}" (${colNames}) VALUES (${placeholders})`)
.run(...values);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize object/array values on insert paths the same way as updates.

updateRecords() stringifies object values before binding, but addRecord() and bulkInsertRecords() pass them through raw. That makes JSON/array columns inconsistent: updating works, while inserting the same payload can fail or persist a different representation.

Also applies to: 933-938

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/adapters/sqlite/sqlite.adapter.ts` around lines 758 -
765, addRecord() and bulkInsertRecords() currently bind object/array values raw
while updateRecords() stringifies them, causing inconsistent storage for
JSON/array columns; update the insert paths used in addRecord(),
bulkInsertRecords() (the code building values, colNames, placeholders and
calling sqliteDb.prepare(...).run(...)) to mirror updateRecords() by mapping
over Object.values(data) and JSON.stringify any value that is an object or array
before passing to .run(...), ensuring consistent serialization for object/array
types across inserts and updates.

Comment on lines +13 to +14
const col = `"${filter.columnName}"`;
switch (filter.operator) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Escape or whitelist dynamic identifiers before interpolating them into SQL.

columnName/sortColumns are inserted verbatim between double quotes here. A crafted value containing " can break out of the identifier context and turn filter/sort/cursor clauses into injectable SQL.

🔒 Suggested hardening
+function quoteIdentifier(name: string): string {
+	return `"${name.replaceAll('"', '""')}"`;
+}
+
 export function buildWhereClause(filters: FilterType[]): {
 	clause: string;
 	values: unknown[];
 } {
@@
 	for (const filter of filters) {
-		const col = `"${filter.columnName}"`;
+		const col = quoteIdentifier(filter.columnName);
@@
 export function buildSortClause(sorts: SortType[] | string, order: SortDirection): string {
 	if (Array.isArray(sorts)) {
 		if (!sorts.length) return "";
-		return `ORDER BY ${sorts.map((s) => `"${s.columnName}" ${s.direction.toUpperCase()}`).join(", ")}`;
+		return `ORDER BY ${sorts.map((s) => `${quoteIdentifier(s.columnName)} ${s.direction === "desc" ? "DESC" : "ASC"}`).join(", ")}`;
 	}
 	if (sorts && typeof sorts === "string") {
-		return `ORDER BY "${sorts}" ${order?.toUpperCase() || "ASC"}`;
+		return `ORDER BY ${quoteIdentifier(sorts)} ${order === "desc" ? "DESC" : "ASC"}`;
 	}
@@
-	const columnList = sortColumns.map((col) => `"${col}"`).join(", ");
+	const columnList = sortColumns.map((col) => quoteIdentifier(col)).join(", ");

Also applies to: 65-71, 93-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/adapters/sqlite/sqlite.query-builder.ts` around lines 13
- 14, The code interpolates raw identifiers (e.g., filter.columnName and entries
in sortColumns) into quoted SQL identifiers (see the line that sets col =
`"${filter.columnName}"` and the other occurrences around sort/ cursor
construction), which allows injection if the value contains quotes; fix by
validating or escaping identifiers before interpolation: either
whitelist/validate each identifier against a safe pattern like
/^[A-Za-z_][A-Za-z0-9_]*$/ (reject or throw on invalid names) or escape double
quotes by replacing " with "" per SQLite identifier quoting rules, then use the
sanitized identifier in place of filter.columnName and in all uses of
sortColumns and cursor column interpolation. Ensure the same sanitization
utility is used consistently in the functions that build filters, sorts, and
cursor clauses (the places referenced around the col assignment and the
sorts/cursors).

Comment on lines +165 to +170
closeSqliteDb(): void {
if (this.sqliteDb) {
this.sqliteDb.close();
this.sqliteDb = null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

SQLite handle is only closed in closeAll(), not in targeted close flows.

closePool() / closePoolByDatabase() still skip SQLite, so selective-close paths can leave the file handle open.

♻️ Proposed fix
 async closePool(connectionString: string): Promise<void> {
+	if (this.baseConfig?.dbType === "sqlite" && connectionString === this.baseConfig.url) {
+		this.closeSqliteDb();
+		return;
+	}
 	await this.closePgPool(connectionString);
 	await this.closeMysqlPool(connectionString);
 	await this.closeMssqlPool(connectionString);
 }

 async closePoolByDatabase(database: string): Promise<void> {
+	if (this.baseConfig?.dbType === "sqlite") {
+		this.closeSqliteDb();
+		return;
+	}
 	const connectionString = this.buildConnectionString(database);
 	await this.closePool(connectionString);
 }

Also applies to: 419-419

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/db-manager.ts` around lines 165 - 170, The SQLite file
handle is only closed by closeSqliteDb() when closeAll() runs, but closePool()
and closePoolByDatabase() skip it and can leave the handle open; update
closePool() and closePoolByDatabase() to call closeSqliteDb() (or perform the
same sqliteDb?.close() and sqliteDb = null logic) whenever the pool being closed
corresponds to the SQLite database (match by the same database identifier used
in those methods), ensuring the sqliteDb is closed and nulled in selective-close
flows as well; keep closeSqliteDb() as the single implementation to avoid
duplication and call it from closePool()/closePoolByDatabase().

Comment on lines +630 to +639
if (normalized === "integer" || normalized === "int") return StandardizedDataType.int;
if (normalized === "bigint") return StandardizedDataType.bigint;
if (normalized === "smallint") return StandardizedDataType.smallint;
if (normalized === "tinyint") return StandardizedDataType.tinyint;
if (normalized === "mediumint") return StandardizedDataType.mediumint;
if (normalized.includes("int")) return StandardizedDataType.int;

if (normalized === "real" || normalized === "float") return StandardizedDataType.float;
if (normalized === "double" || normalized === "double precision")
return StandardizedDataType.double;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Parameterized SQLite numeric types can be mislabeled.

bigint(20) / smallint(6) fall through to includes("int") and become int, and parameterized float/double types may miss their specific labels.

🎯 Proposed fix
-	if (normalized === "bigint") return StandardizedDataType.bigint;
-	if (normalized === "smallint") return StandardizedDataType.smallint;
-	if (normalized === "tinyint") return StandardizedDataType.tinyint;
-	if (normalized === "mediumint") return StandardizedDataType.mediumint;
+	if (normalized === "bigint" || normalized.startsWith("bigint("))
+		return StandardizedDataType.bigint;
+	if (normalized === "smallint" || normalized.startsWith("smallint("))
+		return StandardizedDataType.smallint;
+	if (normalized === "tinyint" || normalized.startsWith("tinyint("))
+		return StandardizedDataType.tinyint;
+	if (normalized === "mediumint" || normalized.startsWith("mediumint("))
+		return StandardizedDataType.mediumint;
 	if (normalized.includes("int")) return StandardizedDataType.int;

-	if (normalized === "real" || normalized === "float") return StandardizedDataType.float;
-	if (normalized === "double" || normalized === "double precision")
+	if (normalized === "real" || normalized === "float" || normalized.startsWith("float("))
+		return StandardizedDataType.float;
+	if (
+		normalized === "double" ||
+		normalized === "double precision" ||
+		normalized.startsWith("double(")
+	)
 		return StandardizedDataType.double;

Also applies to: 641-648

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/types/column.type.ts` around lines 630 - 639, The
type-normalization logic misclassifies parameterized types like "bigint(20)" or
"smallint(6)" because the generic includes("int") branch runs first; update the
comparisons to match whole words or prefixes before the generic match (e.g., use
normalized.startsWith("bigint") / /^bigint(\(|$)/, and similarly for smallint,
tinyint, mediumint) and do the same for float/double (startsWith or regex to
catch "float(8)" / "double precision(10)"); ensure these checks still return the
correct StandardizedDataType values and keep the generic
normalized.includes("int") fallback last (apply the same change to the
corresponding float/double block referenced in the diff).

@husamql3
husamql3 merged commit bde8716 into stage May 9, 2026
5 checks passed
@husamql3
husamql3 deleted the support-sqlite branch May 10, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant