Merge pull request #42 from agnosticeng/remote-engine

feat: use remote ch
This commit is contained in:
Didier Franc
2025-01-03 17:59:26 +01:00
committed by GitHub
17 changed files with 147 additions and 110 deletions

View File

@@ -1,12 +0,0 @@
import { exec } from './query';
export async function init() {
await exec(`
CREATE DATABASE IF NOT EXISTS agx;
USE agx;
`);
}
export { Sources } from './sources.svelte';
export type * from './types';
export { exec };

View File

@@ -1,13 +0,0 @@
import { invoke } from '@tauri-apps/api/core';
import type { CHResponse } from './types';
export async function exec(query: string) {
try {
const r: string = await invoke('query', { query });
if (!r) return;
return JSON.parse(r) as CHResponse;
} catch (e) {
console.error(e);
}
}

View File

@@ -1,32 +0,0 @@
import { exec } from './query';
import type { Source } from './types';
const LIST_SOURCES_QUERY = `select
t.name as name,
t.engine as engine,
groupArray(map(
'name', c.name,
'type', c.type
)) as columns
from system.tables as t
inner join system.columns as c on t.name = c.table
where database = 'agx'
group by t.name, t.engine
`;
export class Sources {
#tables = $state.raw<Source[]>([]);
constructor() {
this.fetch();
}
public get tables() {
return this.#tables;
}
async fetch() {
const response = await exec(LIST_SOURCES_QUERY);
if (response) this.#tables = response.data as Source[];
}
}

View File

@@ -1,15 +0,0 @@
export type CHResponse = {
meta: Array<ColumnDescriptor>;
data: Array<{ [key: string]: any }>;
};
export interface ColumnDescriptor {
name: string;
type: string;
}
export interface Source {
name: string;
engine: string;
columns: ColumnDescriptor[];
}

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import type { Table } from '$lib/olap-engine';
import SearchBar from '$lib/components/SearchBar.svelte';
import { get_app_context } from '$lib/context';
import Database from '$lib/icons/Database.svelte';
import Table from '$lib/icons/Table.svelte';
import TableC from '$lib/icons/Table.svelte';
import {
filter,
remove_nullable,
@@ -10,12 +11,16 @@
SOURCE_TYPE_SHORT_NAME_MAP
} from './utils';
const { sources } = get_app_context();
type Props = {
tables?: Table[];
};
let { tables = [] }: Props = $props();
let loading = $state(false);
let search = $state<string>('');
const filtered = $derived(filter(sources.tables, search));
const filtered = $derived(filter(tables, search));
</script>
<SearchBar bind:value={search} />
@@ -23,9 +28,7 @@
<button
disabled={loading}
onclick={() => {
if (loading) return;
loading = true;
sources.fetch().finally(() => (loading = false));
// @todo: reload
}}>Refresh</button
>
</div>
@@ -36,7 +39,7 @@
{#if source.engine === 'MergeTree'}
<Database size="15" />
{:else}
<Table size="15" />
<TableC size="15" />
{/if}
<h3>{source.name}</h3>
<span class="Tag" style:background-color={SOURCE_TYPE_COLOR_MAP[source.engine]}>

View File

@@ -1,6 +1,6 @@
import { type Source } from '$lib/ch-engine';
import { type Table } from '$lib/olap-engine';
export function filter(sources: Source[], search: string) {
export function filter(sources: Table[], search: string) {
if (!search) return sources;
const search_ = search.toLowerCase();

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { ColumnDescriptor } from '$lib/ch-engine';
import type { Table } from '$lib/ch-engine';
import { sql } from '@codemirror/lang-sql';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, keymap, placeholder } from '@codemirror/view';
@@ -7,20 +7,20 @@
import './codemirror.css';
import { default_extensions, default_keymaps } from './extensions';
import { ProxyDialect } from './SQLDialect';
import { schema_to_completions } from './utils';
import { schema_to_completions, sources_to_schema } from './utils';
type Props = {
value: string;
onExec?: () => unknown;
schema?: { [table_name: string]: ColumnDescriptor[] };
tables?: Table[];
};
let { value = $bindable(''), onExec, schema = {} }: Props = $props();
let { value = $bindable(''), onExec, tables = [] }: Props = $props();
let container: HTMLDivElement;
let editor_view: EditorView;
const dialect_compartment = new Compartment();
const sql_schema = $derived(schema_to_completions(schema));
const sql_schema = $derived(schema_to_completions(sources_to_schema(tables)));
$effect(() => {
editor_view = new EditorView({ parent: container });

View File

@@ -1,9 +1,9 @@
import type { ColumnDescriptor, Source } from '$lib/ch-engine';
import type { ColumnDescriptor, Table } from '$lib/ch-engine';
import type { Completion } from '@codemirror/autocomplete';
export function sources_to_schema(sources: Source[]): {
[table_name: string]: ColumnDescriptor[];
} {
export type Schema = { [table_name: string]: ColumnDescriptor[] };
export function sources_to_schema(sources: Table[]): Schema {
return sources.reduce((acc, k) => {
if (k.columns.length) return { ...acc, [k.name]: k.columns };
return acc;

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import type { Table } from '$lib/ch-engine';
import Datasets from './Datasets/Datasets.svelte';
type Tab = 'sources' | 'queries' | 'history';
@@ -7,6 +8,12 @@
function switch_to(next_tab: Tab) {
tab = next_tab;
}
type Props = {
tables?: Table[];
};
let { tables = [] }: Props = $props();
</script>
<section>
@@ -16,7 +23,7 @@
<button aria-current={tab === 'history'} onclick={() => switch_to('history')}>History</button>
</nav>
{#if tab === 'sources'}
<Datasets />
<Datasets {tables} />
{/if}
</section>

View File

@@ -0,0 +1,29 @@
import { invoke } from '@tauri-apps/api/core';
import type { OLAPEngine, Table, OLAPResponse } from './index';
import CLICKHOUSE_GET_SCHEMA from './queries/clickhouse_get_schema.sql?raw';
import CLICKHOUSE_INIT_DB from './queries/clickhouse_init_db.sql?raw';
export class CHDBEngine implements OLAPEngine {
async init() {
await this.exec(CLICKHOUSE_INIT_DB);
}
async exec(query: string) {
try {
const r: string = await invoke('query', { query });
if (!r) return;
return JSON.parse(r) as OLAPResponse;
} catch (e) {
console.error(e);
return undefined;
}
}
async getSchema() {
const response = await this.exec(CLICKHOUSE_GET_SCHEMA);
if (!response) return [];
return response.data as Table[];
}
}

View File

@@ -0,0 +1,32 @@
import type { OLAPEngine, Table, OLAPResponse } from './index';
import CLICKHOUSE_GET_SCHEMA from './queries/clickhouse_get_schema.sql?raw';
export class RemoteEngine implements OLAPEngine {
async init() {}
async exec(query: string) {
try {
const response = await fetch('https://proxy.agx.app/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: query
});
const r = await response.text();
if (!r) return;
return JSON.parse(r) as OLAPResponse;
} catch (e) {
console.error(e);
return undefined;
}
}
async getSchema() {
const response = await this.exec(CLICKHOUSE_GET_SCHEMA);
if (!response) return [];
return response.data as Table[];
}
}

View File

@@ -0,0 +1,26 @@
import { CHDBEngine } from './engine-chdb';
import { RemoteEngine } from './engine-remote';
export type OLAPResponse = {
meta: Array<ColumnDescriptor>;
data: Array<{ [key: string]: any }>;
};
export interface ColumnDescriptor {
name: string;
type: string;
}
export interface Table {
name: string;
engine: string;
columns: ColumnDescriptor[];
}
export interface OLAPEngine {
init(): Promise<void>;
exec(query: string): Promise<OLAPResponse | undefined>;
getSchema(): Promise<Table[]>;
}
export const engine = new RemoteEngine() as OLAPEngine;

View File

@@ -0,0 +1,11 @@
SELECT
t.name AS name,
t.engine AS engine,
groupArray(map(
'name', c.name,
'type', c.type
)) AS columns
FROM system.tables AS t
INNER JOIN system.columns AS c ON t.name = c.table
WHERE database = currentDatabase()
GROUP BY t.name, t.engine

View File

@@ -0,0 +1,2 @@
CREATE DATABASE IF NOT EXISTS agx;
USE agx;

4
src/lib/types.d.ts vendored
View File

@@ -1,7 +1,5 @@
import type { Sources } from './ch-engine';
type MaybePromise<T> = T | Promise<T>;
export type AppContext = {
sources: Sources;
tables: Table[];
};

View File

@@ -1,14 +1,15 @@
<script lang="ts">
import { exec, Sources, type CHResponse } from '$lib/ch-engine';
import { Editor, sources_to_schema } from '$lib/components/Editor';
import { engine, type OLAPResponse } from '$lib/olap-engine';
import type { Table } from '$lib/olap-engine';
import { Editor } from '$lib/components/Editor';
import Result from '$lib/components/Result.svelte';
import SideBar from '$lib/components/SideBar.svelte';
import { SplitPane } from '$lib/components/SplitPane';
import WindowTitleBar from '$lib/components/WindowTitleBar.svelte';
import { set_app_context } from '$lib/context';
import type { PageData } from './$types';
let response = $state.raw<CHResponse>();
let response = $state.raw<OLAPResponse>();
let { data }: { data: PageData } = $props();
@@ -18,12 +19,16 @@
async function handleExec() {
if (loading) return;
loading = true;
response = await exec(query).finally(() => (loading = false));
response = await engine.exec(query).finally(() => (loading = false));
}
const sources = new Sources();
let tables = $state<Table[]>([]);
set_app_context({ sources });
$effect(() => {
engine.getSchema().then((t) => {
tables = t;
});
});
</script>
<WindowTitleBar>
@@ -35,16 +40,12 @@
<section class="screen">
<SplitPane orientation="horizontal" position="242px" min="242px" max="40%">
{#snippet a()}
<SideBar />
<SideBar {tables} />
{/snippet}
{#snippet b()}
<SplitPane orientation="vertical" min="20%" max="80%" --color="hsl(0deg 0% 12%)">
{#snippet a()}
<Editor
bind:value={query}
onExec={handleExec}
schema={sources_to_schema(sources.tables)}
/>
<Editor bind:value={query} onExec={handleExec} {tables} />
{/snippet}
{#snippet b()}
<Result {response} />

View File

@@ -1,8 +1,8 @@
import { init } from '$lib/ch-engine';
import { engine } from '$lib/olap-engine';
import type { PageLoad } from './$types';
export const load = (async () => {
await init();
await engine.init();
return {};
}) satisfies PageLoad;