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
2 changes: 1 addition & 1 deletion build/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ info:
description: "A fast, native SQL client built with Go, Wails and React." # The application description
copyright: "(c) 2026, Bare7a" # Copyright text
comments: "A fast, native SQL client built with Go, Wails and React." # Comments
version: "1.4.0" # The application version
version: "1.4.1" # The application version
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
# # Should match the name of your .icon file without the extension
# # If not set and Assets.car exists, defaults to "appicon"
Expand Down
4 changes: 2 additions & 2 deletions build/darwin/Info.dev.plist
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>CFBundleVersion</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
<key>NSAppTransportSecurity</key>
Expand Down
4 changes: 2 additions & 2 deletions build/darwin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>CFBundleVersion</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
<key>NSHighResolutionCapable</key>
Expand Down
4 changes: 2 additions & 2 deletions build/ios/Info.dev.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.0-dev</string>
<string>1.4.1-dev</string>
<key>CFBundleVersion</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
Expand Down
4 changes: 2 additions & 2 deletions build/ios/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>CFBundleVersion</key>
<string>1.4.0</string>
<string>1.4.1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
Expand Down
2 changes: 1 addition & 1 deletion build/linux/nfpm/nfpm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
name: "XenSQL"
arch: ${GOARCH}
platform: "linux"
version: "1.4.0"
version: "1.4.1"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
Expand Down
4 changes: 2 additions & 2 deletions build/windows/info.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"fixed": {
"file_version": "1.4.0"
"file_version": "1.4.1"
},
"info": {
"0000": {
"ProductVersion": "1.4.0",
"ProductVersion": "1.4.1",
"CompanyName": "Bare7a",
"FileDescription": "A fast, native SQL client built with Go, Wails and React.",
"LegalCopyright": "(c) 2026, Bare7a",
Expand Down
2 changes: 1 addition & 1 deletion build/windows/nsis/wails_tools.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "XenSQL"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.4.0"
!define INFO_PRODUCTVERSION "1.4.1"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "(c) 2026, Bare7a"
Expand Down
2 changes: 1 addition & 1 deletion build/windows/wails.exe.manifest
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.bare7a.xensql" version="1.4.0" processorArchitecture="*"/>
<assemblyIdentity type="win32" name="com.bare7a.xensql" version="1.4.1" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
Expand Down
40 changes: 39 additions & 1 deletion e2e/specs/editor/autocomplete.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { POSTGRES } from '@support/databases';
import { POSTGRES, SQLITE } from '@support/databases';
import { expect, test } from '@support/fixtures';
import { uniqueIdent } from '@support/seed';

test.describe('Editor autocomplete', () => {
test('shows autocomplete suggestions', async ({ connections, editor }) => {
Expand All @@ -12,4 +13,41 @@ test.describe('Editor autocomplete', () => {
await expect(editor.suggestWidget).toBeVisible();
await expect(editor.suggestWidget.locator('.monaco-list-row').first()).toBeVisible();
});

// SQLite so the test needs no database container.
test('suggests a seeded table, its columns, and the FK join condition', async ({
connections,
editor,
schema,
seed,
app,
}) => {
await connections.createAndConnect(SQLITE);

const parent = await seed.table('ac_parent');
const child = uniqueIdent('ac_child');
await editor.run(`CREATE TABLE ${child} (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES ${parent}(id));`);
await app.expectStatementApplied();
await schema.refresh();

await editor.clear();
await editor.type(`SELECT * FROM ${parent.slice(0, 12)}`);
await editor.triggerSuggestions();
await expect(editor.suggestWidget.locator('.monaco-list-row', { hasText: parent })).toBeVisible();
await editor.page.keyboard.press('Escape');

await editor.clear();
await editor.type(`SELECT * FROM ${parent} WHERE na`);
await editor.triggerSuggestions();
await expect(editor.suggestWidget.locator('.monaco-list-row', { hasText: 'name' }).first()).toBeVisible();
await editor.page.keyboard.press('Escape');

await editor.clear();
await editor.type(`SELECT * FROM ${parent} JOIN ${child} ON `);
await editor.triggerSuggestions();
await expect(
editor.suggestWidget.locator('.monaco-list-row', { hasText: `${child}.parent_id = ${parent}.id` }).first(),
).toBeVisible();
await editor.page.keyboard.press('Escape');
});
});
2 changes: 2 additions & 0 deletions frontend/bindings/xensql/internal/database/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export class ColumnInfo {
"isNullable": boolean;
"isPrimary": boolean;
"isForeign": boolean;
"foreignTable"?: string;
"foreignColumn"?: string;
"defaultVal"?: string;

/** Creates a new ColumnInfo instance. */
Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "xensql-frontend",
"private": true,
"version": "1.4.0",
"version": "1.4.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
20 changes: 18 additions & 2 deletions frontend/src/features/editor/SqlEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { useEditorFontSize } from '@/features/editor/hooks/useEditorFontSize';
import { useJumpToError } from '@/features/editor/hooks/useJumpToError';
import { useRunGlyphs } from '@/features/editor/hooks/useRunGlyphs';
import { useSidebarInsert } from '@/features/editor/hooks/useSidebarInsert';
import { useSqlDiagnostics } from '@/features/editor/hooks/useSqlDiagnostics';
import { createSqlCompletionProvider } from '@/features/editor/lib/createSqlCompletionProvider';
import { createSqlHoverProvider } from '@/features/editor/lib/createSqlHoverProvider';
import { monacoFontOptions } from '@/features/editor/lib/editorFontSize';
import { getMonacoThemeName, setupMonacoBeforeMount } from '@/features/editor/lib/monacoTheme';
import { findStatementAtRunLine } from '@/features/editor/lib/sqlStatements';
Expand Down Expand Up @@ -170,7 +172,8 @@ export const SqlEditor = memo(function SqlEditor({
}
}, []);

const { updateRunGlyphs, statementsRef } = useRunGlyphs(editorRef, monacoRef, sql, languageRevision);
const { updateRunGlyphs, statementsRef } = useRunGlyphs(editorRef, monacoRef, sql, languageRevision, driver);
useSqlDiagnostics(editorRef, monacoRef, sql, allTables, schemas, driver);

const { bindEditorActions } = useEditorActions({
editorRef,
Expand Down Expand Up @@ -198,7 +201,7 @@ export const SqlEditor = memo(function SqlEditor({
if (!monaco || !ed) return;

completionProviderRef.current?.dispose();
completionProviderRef.current = monaco.languages.registerCompletionItemProvider(
const completion = monaco.languages.registerCompletionItemProvider(
'sql',
createSqlCompletionProvider(monaco, ed, () => {
const {
Expand All @@ -211,6 +214,19 @@ export const SqlEditor = memo(function SqlEditor({
return { schemas: s, allTables: at, tablesBySchema: tbs, onLoadColumns: loadCols, driver: drv };
}),
);
const hover = monaco.languages.registerHoverProvider(
'sql',
createSqlHoverProvider(ed, () => {
const { schemas: s, allTables: at, onLoadColumns: loadCols, driver: drv } = completionCtxRef.current;
return { schemas: s, allTables: at, onLoadColumns: loadCols, driver: drv };
}),
);
completionProviderRef.current = {
dispose: () => {
completion.dispose();
hover.dispose();
},
};
}, [isActive]);

const handleEditorMount = (ed: editor.IStandaloneCodeEditor, monaco: Monaco) => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/features/editor/hooks/useRunGlyphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import type { editor } from 'monaco-editor';
import { type RefObject, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { parseSqlStatements, type SqlStatement } from '@/features/editor/lib/sqlStatements';
import type { DriverType } from '@/types';

export function useRunGlyphs(
editorRef: RefObject<editor.IStandaloneCodeEditor | null>,
monacoRef: RefObject<Monaco | null>,
sql: string,
languageRevision: number,
driver: DriverType,
) {
const { t } = useTranslation();
const runGlyphDecorationsRef = useRef<editor.IEditorDecorationsCollection | null>(null);
const statementsRef = useRef<SqlStatement[]>([]);

const updateRunGlyphs = useCallback(
(ed: editor.IStandaloneCodeEditor, monaco: Monaco, text: string) => {
const statements = parseSqlStatements(text);
const statements = parseSqlStatements(text, driver);
statementsRef.current = statements;

const decorations = statements.map((stmt) => ({
Expand All @@ -34,7 +36,7 @@ export function useRunGlyphs(
runGlyphDecorationsRef.current.set(decorations);
}
},
[t],
[t, driver],
);

useEffect(() => {
Expand Down
17 changes: 4 additions & 13 deletions frontend/src/features/editor/hooks/useSchemaPreloader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef } from 'react';
import { useCallback, useEffect } from 'react';
import { api } from '@/shared/lib/api';
import { cachedColumns } from '@/shared/lib/columnCache';
import { useAppStore } from '@/store/appStore';
import type { ColumnInfo, EditorTab } from '@/types';

Expand All @@ -10,9 +11,6 @@ export function useSchemaPreloader(
): {
loadColumnsForConnection: (connectionId: string) => (schema: string, table: string) => Promise<ColumnInfo[]>;
} {
// Session-lifetime column cache keyed by `${connId}:${schema}.${table}`.
const columnsCacheRef = useRef<Record<string, ColumnInfo[]>>({});

const loadSchemaForConnection = useCallback(async (connId: string) => {
if (!connId) return;
const { tables, setConnected, setSchemas, setTables } = useAppStore.getState();
Expand Down Expand Up @@ -42,15 +40,8 @@ export function useSchemaPreloader(

const loadColumnsForConnection = useCallback(
(connectionId: string) =>
async (schema: string, table: string): Promise<ColumnInfo[]> => {
const key = `${connectionId}:${schema}.${table}`;
const cached = columnsCacheRef.current[key];
if (cached?.length) return cached;
const cols = await api.listColumns(connectionId, schema, table);
// Don't cache []: a transient empty miss would hide completions until restart.
if (cols.length) columnsCacheRef.current[key] = cols;
return cols;
},
(schema: string, table: string): Promise<ColumnInfo[]> =>
cachedColumns(connectionId, schema, table, () => api.listColumns(connectionId, schema, table)),
[],
);

Expand Down
50 changes: 50 additions & 0 deletions frontend/src/features/editor/hooks/useSqlDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Monaco } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import { type RefObject, useEffect } from 'react';
import { collectSchemaDiagnostics } from '@/features/editor/lib/sqlDiagnostics';
import type { DriverType, SchemaInfo, TableInfo } from '@/types';

const SCHEMA_MARKER_OWNER = 'xensql-schema';

// Warns on unknown table names; the ref under the caret is skipped so typing doesn't flicker.
export function useSqlDiagnostics(
editorRef: RefObject<editor.IStandaloneCodeEditor | null>,
monacoRef: RefObject<Monaco | null>,
sql: string,
tables: TableInfo[],
schemas: SchemaInfo[],
driver: DriverType,
) {
useEffect(() => {
const ed = editorRef.current;
const monaco = monacoRef.current;
const model = ed?.getModel();
if (!ed || !monaco || !model) return;

const handle = setTimeout(() => {
if (model.isDisposed()) return;
const cursor = ed.getPosition();
const cursorOffset = cursor ? model.getOffsetAt(cursor) : -1;
const markers = collectSchemaDiagnostics(sql, tables, schemas, driver)
.filter((d) => cursorOffset < d.start || cursorOffset > d.end)
.map((d) => {
const s = model.getPositionAt(d.start);
const e = model.getPositionAt(d.end);
return {
severity: monaco.MarkerSeverity.Warning,
message: d.message,
startLineNumber: s.lineNumber,
startColumn: s.column,
endLineNumber: e.lineNumber,
endColumn: e.column,
};
});
monaco.editor.setModelMarkers(model, SCHEMA_MARKER_OWNER, markers);
}, 300);

return () => {
clearTimeout(handle);
if (!model.isDisposed()) monaco.editor.setModelMarkers(model, SCHEMA_MARKER_OWNER, []);
};
}, [editorRef, monacoRef, sql, tables, schemas, driver]);
}
27 changes: 12 additions & 15 deletions frontend/src/features/editor/lib/createSqlCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function createSqlCompletionProvider(
const offset = model.getOffsetAt(position);
// Scope to the current statement so clause context and table bindings don't leak across `;`.
const { start: statementStart, end: statementEnd } = currentStatementRange(
parseSqlStatements(text),
parseSqlStatements(text, driver),
offset,
text.length,
);
Expand All @@ -76,16 +76,11 @@ export function createSqlCompletionProvider(
// Parse only the current statement; loadCols is cached per connection.
const parsed = parseQueryContext(text.slice(statementStart, statementEnd), allTables, schemas, driver);
const columnsByTable: Record<string, ColumnInfo[]> = {};
for (const ref of bindingsNeedingColumns(textBefore, parsed, {
tables: allTables,
schemas,
driver,
})) {
const key = columnCacheKey(ref.schema, ref.table);
if (!columnsByTable[key]) {
columnsByTable[key] = await loadCols(ref.schema, ref.table);
}
}
await Promise.all(
bindingsNeedingColumns(textBefore, parsed, { tables: allTables, schemas, driver }).map(async (ref) => {
columnsByTable[columnCacheKey(ref.schema, ref.table)] = await loadCols(ref.schema, ref.table).catch(() => []);
}),
);

const ctx: CompletionContext = {
schemas,
Expand All @@ -98,10 +93,12 @@ export function createSqlCompletionProvider(

const word = model.getWordUntilPosition(position);
const items = buildCompletionItems({ ctx, text, position: offset, parsed, statementStart });
const range = completionReplaceRange(position, textBefore, {
startColumn: word.startColumn,
endColumn: word.endColumn,
});
const range = completionReplaceRange(
position,
textBefore,
{ startColumn: word.startColumn, endColumn: word.endColumn },
driver,
);
return {
suggestions: items.map((item) => toMonacoCompletion(item, monaco, range)),
};
Expand Down
Loading
Loading