Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query placeholder #239

Merged
merged 10 commits into from
Jan 20, 2025
22 changes: 21 additions & 1 deletion src/components/gui/sql-editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CodeMirror, {
Extension,
ReactCodeMirrorRef,
} from "@uiw/react-codemirror";
import { LanguageSupport } from "@codemirror/language";
import { indentUnit, LanguageSupport } from "@codemirror/language";
import {
acceptCompletion,
completionStatus,
Expand Down Expand Up @@ -83,6 +83,24 @@ const SqlEditor = forwardRef<ReactCodeMirrorRef, SqlEditorProps>(
return true;
},
},
{
key: "Space",
preventDefault: true,
run: (target) => {
if (completionStatus(target.state) === "active") {
acceptCompletion(target);
} else {
invisal marked this conversation as resolved.
Show resolved Hide resolved
target.dispatch({
changes: {
from: target.state.selection.main.from,
insert: " ",
},
selection: { anchor: target.state.selection.main.anchor + 1 },
});
}
return true;
},
},
{
key: "Ctrl-Space",
mac: "Cmd-i",
Expand Down Expand Up @@ -153,6 +171,7 @@ const SqlEditor = forwardRef<ReactCodeMirrorRef, SqlEditorProps>(
},
}),
keyExtensions,
indentUnit.of(" "),
sqlDialect,
tooltipExtension,
tableNameHighlightPlugin,
Expand Down Expand Up @@ -184,6 +203,7 @@ const SqlEditor = forwardRef<ReactCodeMirrorRef, SqlEditorProps>(
drawSelection: false,
}}
theme={theme}
indentWithTab={false}
value={value}
height="100%"
onChange={onChange}
Expand Down
71 changes: 71 additions & 0 deletions src/components/gui/tabs/query-placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { buttonVariants } from "@/components/ui/button";
import { LucideChevronDown } from "lucide-react";
interface Props {
placeHolders: Record<string, string>;
onChange: (placeHolders: Record<string, string>) => void;
}
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";

export function QueryPlaceholder({
placeHolders,
onChange,
}: Props): JSX.Element {
const placeHolderTable = () => {
return (
<div className="overflow-auto p-2">
<table className="border-separate border-spacing-0 w-full text-sm">
<thead className="top-0 sticky">
<tr className="bg-secondary h-[35px] text-xs">
<th className="border-r text-left px-2 border-b">Variable</th>
<th className="border-r text-left px-2 border-b">Value</th>
</tr>
</thead>
<tbody>
{Object.entries(placeHolders).map(([key, value]) => (
<tr key={key}>
<td className="border-r px-4 py-2 border-b">{key}</td>
<td className="border-r px-4 py-2 border-b">
<input
type="text"
className="font-mono bg-inherit w-full h-full outline-none border-0"
value={value ?? ""}
onChange={(e) => {
const newValue = e.currentTarget.value;
const newPlaceHolders = {
...placeHolders,
[key]: newValue,
};
onChange(newPlaceHolders);
}}
></input>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
return (
<Sheet>
<SheetTrigger>
<div className={buttonVariants({ variant: "ghost", size: "sm" })}>
{hasPlaceHolderWithEmptyValue(placeHolders) && (
<div className="flex items-center justify-center h-full pr-2">
<div className="w-2 h-2 bg-orange-900 rounded-full"></div>
</div>
)}
Placeholders{" "}
{!!placeHolders && <LucideChevronDown className="w-4 h-4 ml-2" />}
</div>
</SheetTrigger>
<SheetContent className="p-0">{placeHolderTable()}</SheetContent>
</Sheet>
);
}

export function hasPlaceHolderWithEmptyValue(
placeHolders: Record<string, string>
) {
return Object.values(placeHolders).some((value) => value === "");
}
61 changes: 60 additions & 1 deletion src/components/gui/tabs/query-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { format } from "sql-formatter";
import { useCallback, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
LucideGrid,
LucideMessageSquareWarning,
Expand Down Expand Up @@ -51,6 +51,9 @@ import {
} from "@/components/ui/dropdown-menu";
import { isExplainQueryPlan } from "../query-explanation";
import ExplainResultTab from "../tabs-result/explain-result-tab";
import { tokenizeSql } from "@/lib/sql/tokenizer";
import { QueryPlaceholder } from "./query-placeholder";
import { escapeSqlValue, extractInputValue } from "@/drivers/sqlite/sql-helper";

interface QueryWindowProps {
initialCode?: string;
Expand Down Expand Up @@ -84,6 +87,31 @@ export default function QueryWindow({
initialNamespace ?? "Unsaved Query"
);
const [savedKey, setSavedKey] = useState<string | undefined>(initialSavedKey);
const [placeHolders, setPlaceHolders] = useState<Record<string, string>>({});
invisal marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
const timer = setTimeout(() => {
const editorState = editorRef.current?.view?.state;
if (!editorState) return;
const finalStatements = splitSqlQuery(editorState).map((q) => q.text);
invisal marked this conversation as resolved.
Show resolved Hide resolved
const newPlaceholders: Record<string, string> = {};
for (const statement of finalStatements) {
const token = tokenizeSql(statement);
const placeholders = token
.filter((t) => t.type === "PLACEHOLDER")
.map((t) => t.value.split(":")[1]);
for (const placeholder of placeholders) {
newPlaceholders[placeholder] = "";
}
}
for (const newKey of Object.keys(newPlaceholders)) {
newPlaceholders[newKey] = placeHolders[newKey] ?? "";
}
setPlaceHolders(newPlaceholders);
}, 1000);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [code]);

const onFormatClicked = () => {
try {
Expand Down Expand Up @@ -137,6 +165,29 @@ export default function QueryWindow({
setProgress(undefined);
setQueryTabIndex(0);

//inject placeholders
for (const statement of finalStatements) {
const token = tokenizeSql(statement);
const variables = token
.filter((t) => t.type === "PLACEHOLDER")
.map((t) => t.value.split(":")[1]);
invisal marked this conversation as resolved.
Show resolved Hide resolved
if (
variables.length > 0 &&
variables.some((p) => placeHolders[p] === "")
) {
toast.error("Please fill in all placeholders");
return;
}
}
for (const key of Object.keys(placeHolders)) {
finalStatements = finalStatements.map((s) =>
s.replace(
new RegExp(`:${key}`, "g"),
escapeSqlValue(extractInputValue(placeHolders[key]))
)
invisal marked this conversation as resolved.
Show resolved Hide resolved
);
}

multipleQuery(databaseDriver, finalStatements, (currentProgress) => {
setProgress(currentProgress);
})
Expand Down Expand Up @@ -348,6 +399,14 @@ export default function QueryWindow({
<div>Ln {lineNumber}</div>
<div>Col {columnNumber + 1}</div>
</div>
<div>
{Object.keys(placeHolders).length > 0 && (
<QueryPlaceholder
placeHolders={placeHolders}
invisal marked this conversation as resolved.
Show resolved Hide resolved
onChange={setPlaceHolders}
/>
)}
</div>

<Tooltip>
<TooltipTrigger asChild>
Expand Down
17 changes: 17 additions & 0 deletions src/drivers/sqlite/sql-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ export function escapeSqlValue(value: unknown) {
throw new Error(value.toString() + " is unrecongize type of value");
}

export function extractInputValue(input: string): string | number {
const trimmedInput = input.trim();
if (
(trimmedInput.startsWith('"') && trimmedInput.endsWith('"')) ||
(trimmedInput.startsWith("'") && trimmedInput.endsWith("'"))
) {
return trimmedInput.slice(1, -1).toString();
}

const parsedNumber = parseFloat(trimmedInput);
if (!isNaN(parsedNumber)) {
return parsedNumber;
}

return trimmedInput.toString();
}

export function convertSqliteType(
type: string | undefined
): TableColumnDataType | undefined {
Expand Down
Loading
Loading