-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add executeSQL function * Throw errors if the SQL query fails
- Loading branch information
1 parent
f7d8e0a
commit 864ee9f
Showing
11 changed files
with
244 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# `executeSQL` | ||
|
||
A function that executes a SQL query on a local SQLite database and returns the query result in JSON format. | ||
|
||
## Signature | ||
|
||
```ts | ||
function executeSQL<T = unknown>(databasePath: string, query: string): Promise<T[]> | ||
``` | ||
|
||
### Arguments | ||
|
||
- `databasePath` is the path to the local SQL database. | ||
- `query` is the SQL query to run on the database. | ||
|
||
### Return | ||
|
||
Returns a `Promise` that resolves to an array of objects representing the query results. | ||
|
||
## Example | ||
|
||
```typescript | ||
import { closeMainWindow, Clipboard } from "@raycast/api"; | ||
import { executeSQL } from "@raycast/utils"; | ||
type Message = { body: string; code: string }; | ||
const DB_PATH = "/path/to/chat.db"; | ||
export default async function Command() { | ||
const query = ` | ||
SELECT body, code | ||
FROM message | ||
ORDER BY date DESC | ||
LIMIT 1; | ||
`; | ||
const messages = await executeSQL<Message>(DB_PATH, query); | ||
if (messages.length > 0) { | ||
const latestCode = messages[0].code; | ||
await Clipboard.paste(latestCode); | ||
await closeMainWindow(); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { baseExecuteSQL } from "./sql-utils"; | ||
|
||
/** | ||
* Executes a SQL query on a local SQLite database and returns the query result in JSON format. | ||
* | ||
* @param databasePath - The path to the SQLite database file. | ||
* @param query - The SQL query to execute. | ||
* @returns A Promise that resolves to an array of objects representing the query results. | ||
* | ||
* @example | ||
* ```typescript | ||
* import { closeMainWindow, Clipboard } from "@raycast/api"; | ||
* import { executeSQL } from "@raycast/utils"; | ||
* | ||
* type Message = { body: string; code: string }; | ||
* | ||
* const DB_PATH = "/path/to/chat.db"; | ||
* | ||
* export default async function Command() { | ||
* const query = `SELECT body, code FROM ...` | ||
* | ||
* const messages = await executeSQL<Message>(DB_PATH, query); | ||
* | ||
* if (messages.length > 0) { | ||
* const latestCode = messages[0].code; | ||
* await Clipboard.paste(latestCode); | ||
* await closeMainWindow(); | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
export function executeSQL<T = unknown>(databasePath: string, query: string) { | ||
return baseExecuteSQL<T>(databasePath, query); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { existsSync } from "node:fs"; | ||
import { copyFile, mkdir, writeFile } from "node:fs/promises"; | ||
import os from "node:os"; | ||
import childProcess from "node:child_process"; | ||
import path from "node:path"; | ||
import { getSpawnedPromise, getSpawnedResult } from "./exec-utils"; | ||
import { hash } from "./helpers"; | ||
|
||
export class PermissionError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = "PermissionError"; | ||
} | ||
} | ||
|
||
export function isPermissionError(error: unknown): error is PermissionError { | ||
return error instanceof Error && error.name === "PermissionError"; | ||
} | ||
|
||
export async function baseExecuteSQL<T = unknown>( | ||
databasePath: string, | ||
query: string, | ||
options?: { | ||
signal?: AbortSignal; | ||
}, | ||
): Promise<T[]> { | ||
if (!existsSync(databasePath)) { | ||
throw new Error("The database does not exist"); | ||
} | ||
|
||
const abortSignal = options?.signal; | ||
let workaroundCopiedDb: string | undefined; | ||
|
||
let spawned = childProcess.spawn("sqlite3", ["--json", "--readonly", databasePath, query], { signal: abortSignal }); | ||
let spawnedPromise = getSpawnedPromise(spawned); | ||
let [{ error, exitCode, signal }, stdoutResult, stderrResult] = await getSpawnedResult<string>( | ||
spawned, | ||
{ encoding: "utf-8" }, | ||
spawnedPromise, | ||
); | ||
checkAborted(abortSignal); | ||
|
||
if (stderrResult.match("(5)") || stderrResult.match("(14)")) { | ||
// That means that the DB is busy because of another app is locking it | ||
// This happens when Chrome or Arc is opened: they lock the History db. | ||
// As an ugly workaround, we duplicate the file and read that instead | ||
// (with vfs unix - none to just not care about locks) | ||
if (!workaroundCopiedDb) { | ||
const tempFolder = path.join(os.tmpdir(), "useSQL", hash(databasePath)); | ||
await mkdir(tempFolder, { recursive: true }); | ||
checkAborted(abortSignal); | ||
|
||
workaroundCopiedDb = path.join(tempFolder, "db.db"); | ||
await copyFile(databasePath, workaroundCopiedDb); | ||
|
||
await writeFile(workaroundCopiedDb + "-shm", ""); | ||
await writeFile(workaroundCopiedDb + "-wal", ""); | ||
|
||
checkAborted(abortSignal); | ||
} | ||
|
||
spawned = childProcess.spawn("sqlite3", ["--json", "--readonly", "--vfs", "unix-none", workaroundCopiedDb, query], { | ||
signal: abortSignal, | ||
}); | ||
spawnedPromise = getSpawnedPromise(spawned); | ||
[{ error, exitCode, signal }, stdoutResult, stderrResult] = await getSpawnedResult<string>( | ||
spawned, | ||
{ encoding: "utf-8" }, | ||
spawnedPromise, | ||
); | ||
checkAborted(abortSignal); | ||
} | ||
|
||
if (error || exitCode !== 0 || signal !== null) { | ||
if (stderrResult.includes("authorization denied")) { | ||
throw new PermissionError("You do not have permission to access the database."); | ||
} else { | ||
throw new Error(stderrResult || "Unknown error"); | ||
} | ||
} | ||
|
||
return JSON.parse(stdoutResult.trim() || "[]") as T[]; | ||
} | ||
|
||
function checkAborted(signal?: AbortSignal) { | ||
if (signal?.aborted) { | ||
const error = new Error("aborted"); | ||
error.name = "AbortError"; | ||
throw error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.