Add embed query result tool
This commit is contained in:
@@ -28,11 +28,49 @@ mod bindgen {
|
||||
#[wasm_bindgen(method, js_name = getValue)]
|
||||
pub fn get_value(this: &Editor) -> String;
|
||||
|
||||
#[wasm_bindgen(method, js_name = clearSelection)]
|
||||
pub fn clear_selection(this: &Editor);
|
||||
|
||||
#[wasm_bindgen(method, js_name = setValue)]
|
||||
pub fn set_value(this: &Editor, value: String);
|
||||
|
||||
#[wasm_bindgen(method, js_name = getSession)]
|
||||
pub fn get_session(this: &Editor) -> EditSession;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getSelection)]
|
||||
pub fn get_selection(this: &Editor) -> Selection;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getSelectedText)]
|
||||
pub fn get_selected_text(this: &Editor) -> String;
|
||||
|
||||
#[wasm_bindgen(method, js_name = setReadOnly)]
|
||||
pub fn set_read_only(this: &Editor, value: bool);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
pub type Selection;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getRange)]
|
||||
pub fn get_range(this: &Selection) -> JsValue;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
pub type EditSession;
|
||||
|
||||
#[wasm_bindgen(method, js_name = setValue)]
|
||||
pub fn set_value(this: &EditSession, value: String);
|
||||
|
||||
#[wasm_bindgen(method, js_name = getLength)]
|
||||
pub fn get_length(this: &EditSession) -> usize;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getTextRange)]
|
||||
pub fn get_text_range(this: &EditSession, range: JsValue) -> String;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getLine)]
|
||||
pub fn get_line(this: &EditSession, row: usize) -> String;
|
||||
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -81,6 +119,20 @@ impl EditorOptionsBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Range {
|
||||
pub start: Point,
|
||||
pub end: Point,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Point {
|
||||
pub row: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorOptions {
|
||||
@@ -206,9 +258,32 @@ impl Editor {
|
||||
|
||||
pub fn set_value(&self, value: String) {
|
||||
self.js.set_value(value);
|
||||
self.js.clear_selection();
|
||||
}
|
||||
|
||||
pub fn get_range(&self) -> Range {
|
||||
serde_wasm_bindgen::from_value(self.js.get_selection().get_range()).unwrap()
|
||||
}
|
||||
|
||||
pub fn get_selected_value(&self) -> String {
|
||||
self.js.get_selected_text()
|
||||
}
|
||||
|
||||
pub fn set_read_only(&self, value: bool) {
|
||||
self.js.set_read_only(value);
|
||||
}
|
||||
|
||||
pub fn get_length(&self) -> usize {
|
||||
self.js.get_session().get_length()
|
||||
}
|
||||
|
||||
pub fn get_line(&self, row: usize) -> String {
|
||||
self.js.get_session().get_line(row)
|
||||
}
|
||||
|
||||
pub fn get_text_range(&self, range: Range) -> String {
|
||||
self.js
|
||||
.get_session()
|
||||
.get_text_range(serde_wasm_bindgen::to_value(&range).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@ use crate::app::{
|
||||
pub fn AdvancedOptionsMenu() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let value = move || *state.run_selected_sql().read();
|
||||
let is_default = move || !*state.run_selected_sql().read();
|
||||
let on_change = move |value: &bool| {
|
||||
state.run_selected_sql().set(*value);
|
||||
};
|
||||
|
||||
view! {
|
||||
<>
|
||||
<MenuGroup title="Advanced options".into()>
|
||||
@@ -25,9 +19,11 @@ pub fn AdvancedOptionsMenu() -> impl IntoView {
|
||||
b=false
|
||||
a_label=Some("On".to_string())
|
||||
b_label=Some("Off".to_string())
|
||||
value=value
|
||||
is_default=Box::new(is_default)
|
||||
on_change=on_change
|
||||
value=move || *state.run_selected_sql().read()
|
||||
is_default=Box::new(move || !*state.run_selected_sql().read())
|
||||
on_change=move |value: &bool| {
|
||||
state.run_selected_sql().set(*value);
|
||||
}
|
||||
/>
|
||||
</MenuGroup>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use istyles::istyles;
|
||||
use leptos::{html::Input, prelude::*, tachys::html};
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use reactive_stores::Store;
|
||||
use sqlformat::{FormatOptions, QueryParams};
|
||||
use wasm_bindgen::{JsCast, prelude::Closure};
|
||||
use web_sys::{Blob, Event, FileReader, HtmlInputElement, MouseEvent, Url, UrlSearchParams};
|
||||
|
||||
use crate::{
|
||||
FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, SQLiteStatementResult,
|
||||
WorkerRequest,
|
||||
FragileComfirmed, LoadDbOptions, RunOptions, SQLightError, WorkerRequest,
|
||||
app::{
|
||||
ImportProgress,
|
||||
advanced_options_menu::AdvancedOptionsMenu,
|
||||
@@ -76,31 +75,31 @@ pub fn Header() -> impl IntoView {
|
||||
|
||||
pub fn execute(state: Store<GlobalState>) -> Box<dyn Fn() + Send + 'static> {
|
||||
Box::new(move || {
|
||||
let Some((code, selected_code)) = state
|
||||
.editor()
|
||||
.read_untracked()
|
||||
.as_ref()
|
||||
.map(|editor| (editor.get_value(), editor.get_selected_value()))
|
||||
else {
|
||||
let editor_guard = state.editor().read_untracked();
|
||||
let Some(editor) = editor_guard.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let run_selected_code = state.run_selected_sql().get();
|
||||
let (code, selected_code) = (editor.get_value(), editor.get_selected_value());
|
||||
|
||||
drop(editor_guard);
|
||||
|
||||
state.sql().set(code.clone());
|
||||
change_focus(state, Some(Focus::Execute));
|
||||
std::mem::take(&mut *state.output().write());
|
||||
|
||||
let run_selected_code =
|
||||
!selected_code.is_empty() && state.run_selected_sql().get_untracked();
|
||||
|
||||
if let Some(worker) = &*state.worker().read_untracked() {
|
||||
worker.send_task(WorkerRequest::Prepare(PrepareOptions {
|
||||
sql: if !selected_code.is_empty() && run_selected_code {
|
||||
worker.send_task(WorkerRequest::Run(RunOptions {
|
||||
embed: false,
|
||||
sql: if run_selected_code {
|
||||
selected_code
|
||||
} else {
|
||||
code
|
||||
},
|
||||
clear_on_prepare: !*state.keep_ctx().read_untracked(),
|
||||
}));
|
||||
worker.send_task(WorkerRequest::Continue);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -209,6 +208,8 @@ fn AdvancedOptionsMenuButton(menu_container: NodeRef<html::element::Div>) -> imp
|
||||
|
||||
#[component]
|
||||
fn ToolsButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let button = |toggle, node_ref| {
|
||||
view! {
|
||||
<Button icon_right=expandable_icon() on_click=toggle node_ref=node_ref>
|
||||
@@ -218,10 +219,62 @@ fn ToolsButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
|
||||
.into_any()
|
||||
};
|
||||
|
||||
let on_format = move |_event, signal: WriteSignal<bool>| {
|
||||
let Some(editor) = &*state.editor().read() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let format_options = FormatOptions {
|
||||
uppercase: Some(true),
|
||||
lines_between_queries: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let sql = sqlformat::format(
|
||||
&editor.get_value(),
|
||||
&QueryParams::default(),
|
||||
&format_options,
|
||||
);
|
||||
|
||||
editor.set_value(sql);
|
||||
signal.set(false);
|
||||
};
|
||||
|
||||
let on_embed = move |_event, signal: WriteSignal<bool>| {
|
||||
let editor_guard = state.editor().read_untracked();
|
||||
let Some(editor) = editor_guard.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let sql = editor.get_value();
|
||||
drop(editor_guard);
|
||||
|
||||
if let Some(worker) = &*state.worker().read_untracked() {
|
||||
worker.send_task(WorkerRequest::Run(RunOptions {
|
||||
embed: true,
|
||||
sql,
|
||||
clear_on_prepare: !*state.keep_ctx().read_untracked(),
|
||||
}));
|
||||
}
|
||||
|
||||
signal.set(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<PopButton
|
||||
button=button
|
||||
menu=Box::new(|_close| { view! { <ToolsMenu /> }.into_any() })
|
||||
menu=Box::new(move |signal: WriteSignal<bool>| {
|
||||
view! {
|
||||
<ToolsMenu
|
||||
on_format=move |e| {
|
||||
on_format(e, signal);
|
||||
}
|
||||
on_embed=move |e| {
|
||||
on_embed(e, signal);
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
menu_container=menu_container
|
||||
></PopButton>
|
||||
}
|
||||
@@ -317,7 +370,7 @@ fn DatabaseButton(
|
||||
}
|
||||
};
|
||||
|
||||
let on_load = move |_: MouseEvent| {
|
||||
let on_load = move |_: MouseEvent, signal: WriteSignal<bool>| {
|
||||
if let Some(input) = &*input_ref.read() {
|
||||
if input.onchange().is_none() {
|
||||
let callback = Closure::wrap(Box::new(on_change) as Box<dyn Fn(Event)>);
|
||||
@@ -327,6 +380,7 @@ fn DatabaseButton(
|
||||
input.set_value("");
|
||||
input.click();
|
||||
}
|
||||
signal.set(false);
|
||||
};
|
||||
|
||||
Effect::new(move || {
|
||||
@@ -357,17 +411,24 @@ fn DatabaseButton(
|
||||
Url::revoke_object_url(&url).unwrap();
|
||||
});
|
||||
|
||||
let on_download = move |_| {
|
||||
let on_download = move |_: MouseEvent, signal: WriteSignal<bool>| {
|
||||
if let Some(worker) = &*state.worker().read() {
|
||||
worker.send_task(WorkerRequest::DownloadDb);
|
||||
}
|
||||
signal.set(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<PopButton
|
||||
button=button
|
||||
menu=Box::new(move |_close| {
|
||||
view! { <DatabaseMenu load=on_load download=on_download /> }.into_any()
|
||||
menu=Box::new(move |signal| {
|
||||
view! {
|
||||
<DatabaseMenu
|
||||
load=move |e| on_load(e, signal)
|
||||
download=move |e| on_download(e, signal)
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
menu_container=menu_container
|
||||
></PopButton>
|
||||
@@ -388,42 +449,6 @@ fn ShareButton() -> impl IntoView {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut sqls = vec![];
|
||||
|
||||
for result in &*state.output().read() {
|
||||
let mut table_s = Table::new();
|
||||
|
||||
match result {
|
||||
SQLiteStatementResult::Finish => continue,
|
||||
SQLiteStatementResult::Step(table) => {
|
||||
let mut sql = table.sql.trim().to_string();
|
||||
|
||||
if let Some(values) = &table.values {
|
||||
table_s.add_row(Row::new(
|
||||
values.columns.iter().map(|s| Cell::new(s)).collect(),
|
||||
));
|
||||
for row in &values.rows {
|
||||
table_s.add_row(Row::new(row.iter().map(|s| Cell::new(s)).collect()));
|
||||
}
|
||||
|
||||
let result = table_s
|
||||
.to_string()
|
||||
.lines()
|
||||
.map(|x| format!("-- {x}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
sql.push('\n');
|
||||
sql.push_str(&result);
|
||||
}
|
||||
|
||||
sqls.push(sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.share_sql_with_result().set(Some(sqls.join("\n\n")));
|
||||
|
||||
if let Ok(href) = window().location().href().and_then(|href| {
|
||||
let url = Url::new(&href)?;
|
||||
let params = UrlSearchParams::new()?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Store, StoreFieldIterator};
|
||||
use reactive_stores::Store;
|
||||
|
||||
use crate::app::{
|
||||
output::{header::Header, loader::Loader, section::Section, simple_pane::SimplePane},
|
||||
@@ -74,30 +74,38 @@ fn Output() -> AnyView {
|
||||
<Loader />
|
||||
</Show>
|
||||
|
||||
<For
|
||||
each=move || state.output().iter_unkeyed().enumerate()
|
||||
key=|(idx, _)| *idx
|
||||
children=move |(idx, item)| {
|
||||
match &*item.read() {
|
||||
SQLiteStatementResult::Finish => {
|
||||
view! { <Header label="Finished".into() /> }.into_any()
|
||||
}
|
||||
SQLiteStatementResult::Step(table) => {
|
||||
let label = format!("Statement #{}", idx + 1);
|
||||
if let Some(output) = get_output(table) {
|
||||
view! {
|
||||
<Section label=label>
|
||||
<p>{output}</p>
|
||||
</Section>
|
||||
<>
|
||||
{move || {
|
||||
state
|
||||
.output()
|
||||
.read()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, item)| {
|
||||
match &item {
|
||||
SQLiteStatementResult::Finish => {
|
||||
|
||||
view! { <Header label="Finished".into() /> }
|
||||
.into_any()
|
||||
}
|
||||
SQLiteStatementResult::Step(table) => {
|
||||
let label = format!("Statement #{}", idx + 1);
|
||||
if let Some(output) = get_output(table) {
|
||||
view! {
|
||||
<Section label=label>
|
||||
<p>{output}</p>
|
||||
</Section>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
</>
|
||||
</>
|
||||
}
|
||||
.into_any()
|
||||
|
||||
@@ -3,9 +3,7 @@ use std::{sync::Arc, time::Duration};
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{Blob, BlobPropertyBag, Url};
|
||||
|
||||
use crate::app::{GlobalState, GlobalStateStoreFields, icon::clipboard_icon};
|
||||
|
||||
@@ -58,55 +56,11 @@ fn EmbeddedLinks() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SQLWithResultLinks() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let shared = move || {
|
||||
state
|
||||
.share_sql_with_result()
|
||||
.get_untracked()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let href = move || {
|
||||
let text = state
|
||||
.share_sql_with_result()
|
||||
.get_untracked()
|
||||
.unwrap_or_default();
|
||||
let string_array = js_sys::Array::new();
|
||||
string_array.push(&JsValue::from(text));
|
||||
|
||||
let blob_properties = BlobPropertyBag::new();
|
||||
blob_properties.set_type("text/plain;charset=UTF-8");
|
||||
|
||||
let blob =
|
||||
Blob::new_with_str_sequence_and_options(&string_array, &blob_properties).unwrap();
|
||||
|
||||
let url = Url::create_object_url_with_blob(&blob).unwrap();
|
||||
let url1 = url.clone();
|
||||
|
||||
set_timeout(
|
||||
move || Url::revoke_object_url(&url1).unwrap(),
|
||||
Duration::from_millis(5000),
|
||||
);
|
||||
|
||||
url
|
||||
};
|
||||
|
||||
view! {
|
||||
<Copied shared=shared href=href>
|
||||
"[Need run first] Copy sql with result"
|
||||
</Copied>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Share() -> impl IntoView {
|
||||
view! {
|
||||
<>
|
||||
<EmbeddedLinks />
|
||||
<SQLWithResultLinks />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::ops::Deref;
|
||||
use istyles::istyles;
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html;
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use reactive_stores::Store;
|
||||
use split_grid::{Gutter, SplitOptions};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
@@ -11,7 +12,7 @@ use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::wasm_bindgen::JsValue;
|
||||
|
||||
use crate::{
|
||||
SQLightError,
|
||||
SQLightError, SQLiteStatementResult,
|
||||
app::{
|
||||
Focus,
|
||||
editor::Editor,
|
||||
@@ -42,6 +43,7 @@ pub fn playground(
|
||||
handle_save_state(state);
|
||||
handle_import_progress(state);
|
||||
handle_ace_config(state);
|
||||
handle_embed_query_result(state);
|
||||
|
||||
spawn_local(handle_state(state, rx));
|
||||
|
||||
@@ -55,6 +57,62 @@ pub fn playground(
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_embed_query_result(state: Store<GlobalState>) {
|
||||
const MARK: &str = "-- R:";
|
||||
|
||||
Effect::new(move || {
|
||||
let mut sqls = vec![];
|
||||
|
||||
for result in &*state.embed().read() {
|
||||
let mut table_s = Table::new();
|
||||
|
||||
match result {
|
||||
SQLiteStatementResult::Finish => continue,
|
||||
SQLiteStatementResult::Step(table) => {
|
||||
let sql = table.sql.trim().to_string();
|
||||
|
||||
let sql = if let Some(values) = &table.values {
|
||||
table_s.add_row(Row::new(
|
||||
values.columns.iter().map(|s| Cell::new(s)).collect(),
|
||||
));
|
||||
for row in &values.rows {
|
||||
table_s.add_row(Row::new(row.iter().map(|s| Cell::new(s)).collect()));
|
||||
}
|
||||
|
||||
let mut result = table_s
|
||||
.to_string()
|
||||
.lines()
|
||||
.map(|x| format!("{MARK} {x}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
result.push('\n');
|
||||
|
||||
let sql = sql
|
||||
.lines()
|
||||
.filter(|s| !s.starts_with(MARK))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
result.push_str(&sql);
|
||||
|
||||
result
|
||||
} else {
|
||||
sql
|
||||
};
|
||||
|
||||
sqls.push(sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !sqls.is_empty() {
|
||||
if let Some(editor) = &*state.editor().read_untracked() {
|
||||
editor.set_value(sqls.join("\n\n"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_ace_config(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
let config = state.editor_config().read();
|
||||
|
||||
@@ -22,11 +22,10 @@ pub fn PopButton<B, M>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
B: FnOnce(Box<dyn FnMut(MouseEvent) + Send>, NodeRef<html::element::Button>) -> AnyView,
|
||||
M: Fn(Box<dyn Fn()>) -> AnyView + Send + Sync + 'static,
|
||||
M: Fn(WriteSignal<bool>) -> AnyView + Send + Sync + 'static,
|
||||
{
|
||||
let (is_open, set_open) = signal(false);
|
||||
let toggle = move || set_open.set(!is_open.get());
|
||||
let close = move || set_open.set(false);
|
||||
|
||||
let arrow_ref = NodeRef::<html::element::Div>::new();
|
||||
let reference_ref = NodeRef::<html::element::Button>::new();
|
||||
@@ -202,7 +201,7 @@ where
|
||||
</svg>
|
||||
</div>
|
||||
<div node_ref=menu_ref>
|
||||
<div>{menu_clone(Box::new(close))}</div>
|
||||
<div>{menu_clone(set_open)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -48,12 +48,12 @@ pub struct GlobalState {
|
||||
#[serde(skip)]
|
||||
share_href: Option<String>,
|
||||
#[serde(skip)]
|
||||
share_sql_with_result: Option<String>,
|
||||
#[serde(skip)]
|
||||
show_something: bool,
|
||||
#[serde(skip)]
|
||||
output: Vec<SQLiteStatementResult>,
|
||||
#[serde(skip)]
|
||||
embed: Vec<SQLiteStatementResult>,
|
||||
#[serde(skip)]
|
||||
last_error: Option<FragileComfirmed<SQLightError>>,
|
||||
#[serde(skip)]
|
||||
import_progress: Option<ImportProgress>,
|
||||
@@ -77,9 +77,9 @@ impl Default for GlobalState {
|
||||
is_focused: false,
|
||||
opened_focus: HashSet::new(),
|
||||
share_href: None,
|
||||
share_sql_with_result: None,
|
||||
show_something: false,
|
||||
output: Vec::new(),
|
||||
output: vec![],
|
||||
embed: vec![],
|
||||
last_error: None,
|
||||
import_progress: None,
|
||||
exported: None,
|
||||
|
||||
@@ -1,41 +1,22 @@
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::Store;
|
||||
use sqlformat::{FormatOptions, QueryParams};
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
use crate::app::{
|
||||
GlobalState, GlobalStateStoreFields, button_menu_item::ButtonMenuItem, menu_aside::MenuAside,
|
||||
menu_group::MenuGroup,
|
||||
};
|
||||
use crate::app::{button_menu_item::ButtonMenuItem, menu_aside::MenuAside, menu_group::MenuGroup};
|
||||
|
||||
#[component]
|
||||
pub fn ToolsMenu() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
let format = move |_event| {
|
||||
let Some(editor) = &*state.editor().read() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let format_options = FormatOptions {
|
||||
uppercase: Some(true),
|
||||
lines_between_queries: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let sql = sqlformat::format(
|
||||
&editor.get_value(),
|
||||
&QueryParams::default(),
|
||||
&format_options,
|
||||
);
|
||||
|
||||
editor.set_value(sql);
|
||||
};
|
||||
|
||||
pub fn ToolsMenu<F, E>(on_format: F, on_embed: E) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + Send + 'static,
|
||||
E: Fn(MouseEvent) + Send + 'static,
|
||||
{
|
||||
view! {
|
||||
<MenuGroup title="Tools".into()>
|
||||
<ButtonMenuItem name="SQL Format".into() on_click=format>
|
||||
<div>Format this code with sqlformat.</div>
|
||||
<ButtonMenuItem name="SQL Format".into() on_click=on_format>
|
||||
<MenuAside>"https://crates.io/crates/sqlformat"</MenuAside>
|
||||
</ButtonMenuItem>
|
||||
<ButtonMenuItem name="Embed Query Result".into() on_click=on_embed>
|
||||
<MenuAside>"https://crates.io/crates/prettytable-rs"</MenuAside>
|
||||
</ButtonMenuItem>
|
||||
</MenuGroup>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,7 @@ async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiv
|
||||
let request = serde_wasm_bindgen::from_value::<WorkerRequest>(request).unwrap();
|
||||
let resp = match request {
|
||||
WorkerRequest::Open(options) => WorkerResponse::Open(worker::open(options).await),
|
||||
WorkerRequest::Prepare(options) => {
|
||||
WorkerResponse::Prepare(worker::prepare(options).await)
|
||||
}
|
||||
WorkerRequest::Continue => WorkerResponse::Continue(worker::r#continue().await),
|
||||
WorkerRequest::StepOver => WorkerResponse::StepOver(worker::step_over().await),
|
||||
WorkerRequest::StepIn => WorkerResponse::StepIn(worker::step_in().await),
|
||||
WorkerRequest::StepOut => WorkerResponse::StepOut(worker::step_out().await),
|
||||
WorkerRequest::Run(options) => WorkerResponse::Run(worker::run(options).await),
|
||||
WorkerRequest::LoadDb(options) => {
|
||||
WorkerResponse::LoadDb(worker::load_db(options).await)
|
||||
}
|
||||
|
||||
38
src/lib.rs
38
src/lib.rs
@@ -94,11 +94,7 @@ pub enum WorkerError {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum WorkerRequest {
|
||||
Open(OpenOptions),
|
||||
Prepare(PrepareOptions),
|
||||
Continue,
|
||||
StepOver,
|
||||
StepIn,
|
||||
StepOut,
|
||||
Run(RunOptions),
|
||||
LoadDb(LoadDbOptions),
|
||||
DownloadDb,
|
||||
}
|
||||
@@ -107,11 +103,7 @@ pub enum WorkerRequest {
|
||||
pub enum WorkerResponse {
|
||||
Ready,
|
||||
Open(Result<()>),
|
||||
Prepare(Result<()>),
|
||||
Continue(Result<Vec<SQLiteStatementResult>>),
|
||||
StepOver(Result<SQLiteStatementResult>),
|
||||
StepIn(Result<()>),
|
||||
StepOut(Result<SQLiteStatementResult>),
|
||||
Run(Result<SQLiteRunResult>),
|
||||
LoadDb(Result<()>),
|
||||
DownloadDb(Result<DownloadDbResponse>),
|
||||
}
|
||||
@@ -146,8 +138,9 @@ impl OpenOptions {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PrepareOptions {
|
||||
pub struct RunOptions {
|
||||
pub sql: String,
|
||||
pub embed: bool,
|
||||
pub clear_on_prepare: bool,
|
||||
}
|
||||
|
||||
@@ -157,6 +150,12 @@ pub struct InnerError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SQLiteRunResult {
|
||||
embed: bool,
|
||||
result: Vec<SQLiteStatementResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum SQLiteStatementResult {
|
||||
Finish,
|
||||
@@ -168,7 +167,6 @@ pub struct SQLiteStatementTable {
|
||||
pub sql: String,
|
||||
pub position: [usize; 2],
|
||||
pub values: Option<SQLiteStatementValues>,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -253,18 +251,16 @@ pub async fn handle_state(state: Store<GlobalState>, mut rx: UnboundedReceiver<W
|
||||
state.last_error().set(Some(SQLightError::new_worker(err)));
|
||||
}
|
||||
}
|
||||
WorkerResponse::Prepare(result) => {
|
||||
if let Err(err) = result {
|
||||
state.last_error().set(Some(SQLightError::new_worker(err)));
|
||||
WorkerResponse::Run(result) => match result {
|
||||
Ok(SQLiteRunResult { embed, result }) => {
|
||||
if embed {
|
||||
state.embed().set(result);
|
||||
} else {
|
||||
state.output().set(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkerResponse::Continue(result) => match result {
|
||||
Ok(results) => state.output().set(results),
|
||||
Err(err) => state.last_error().set(Some(SQLightError::new_worker(err))),
|
||||
},
|
||||
WorkerResponse::StepOver(_)
|
||||
| WorkerResponse::StepIn(_)
|
||||
| WorkerResponse::StepOut(_) => unimplemented!(),
|
||||
WorkerResponse::LoadDb(result) => {
|
||||
let keep_ctx = result.is_ok();
|
||||
if let Some(progress) = &mut *state.import_progress().write() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
mod sqlitend;
|
||||
|
||||
use crate::{
|
||||
DownloadDbResponse, LoadDbOptions, OpenOptions, PERSIST_VFS, PrepareOptions,
|
||||
SQLiteStatementResult, WorkerError,
|
||||
DownloadDbResponse, LoadDbOptions, OpenOptions, PERSIST_VFS, RunOptions, SQLiteRunResult,
|
||||
WorkerError,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use sqlite_wasm_rs::{
|
||||
@@ -10,7 +10,7 @@ use sqlite_wasm_rs::{
|
||||
mem_vfs::MemVfsUtil,
|
||||
utils::{copy_to_uint8_array, copy_to_vec},
|
||||
};
|
||||
use sqlitend::{SQLiteDb, SQLitePreparedStatement, SQLiteStatements};
|
||||
use sqlitend::SQLiteDb;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::OnceCell;
|
||||
@@ -30,19 +30,13 @@ struct FSUtil {
|
||||
}
|
||||
|
||||
struct SQLiteWorker {
|
||||
db: Option<Arc<SQLiteDb>>,
|
||||
open_options: OpenOptions,
|
||||
state: SQLiteState,
|
||||
}
|
||||
|
||||
enum SQLiteState {
|
||||
Idie,
|
||||
Prepared(PreparedState),
|
||||
}
|
||||
|
||||
struct PreparedState {
|
||||
stmts: SQLiteStatements,
|
||||
prepared: Option<SQLitePreparedStatement>,
|
||||
NotOpened,
|
||||
Opened(Arc<SQLiteDb>),
|
||||
}
|
||||
|
||||
async fn with_worker<F, T>(mut f: F) -> Result<T>
|
||||
@@ -100,7 +94,7 @@ pub async fn load_db(options: LoadDbOptions) -> Result<()> {
|
||||
let db = copy_to_vec(&options.data);
|
||||
|
||||
with_worker(|worker| {
|
||||
worker.db.take();
|
||||
let _ = std::mem::replace(&mut worker.state, SQLiteState::NotOpened);
|
||||
|
||||
let filename = &worker.open_options.filename;
|
||||
if worker.open_options.persist {
|
||||
@@ -117,8 +111,7 @@ pub async fn load_db(options: LoadDbOptions) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
worker.db = Some(SQLiteDb::open(&worker.open_options.uri())?);
|
||||
worker.state = SQLiteState::Idie;
|
||||
worker.state = SQLiteState::Opened(SQLiteDb::open(&worker.open_options.uri())?);
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -132,20 +125,19 @@ pub async fn open(options: OpenOptions) -> Result<()> {
|
||||
init_opfs_util().await?;
|
||||
}
|
||||
|
||||
let db = SQLiteDb::open(&options.uri())?;
|
||||
let state = SQLiteState::Opened(SQLiteDb::open(&options.uri())?);
|
||||
let worker = SQLiteWorker {
|
||||
db: Some(db),
|
||||
open_options: options,
|
||||
state: SQLiteState::Idie,
|
||||
state,
|
||||
};
|
||||
*locker = Some(worker);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn prepare(options: PrepareOptions) -> Result<()> {
|
||||
pub async fn run(options: RunOptions) -> Result<SQLiteRunResult> {
|
||||
with_worker(|worker| {
|
||||
if options.clear_on_prepare {
|
||||
worker.db.take();
|
||||
let _ = std::mem::replace(&mut worker.state, SQLiteState::NotOpened);
|
||||
|
||||
let filename = &worker.open_options.filename;
|
||||
if worker.open_options.persist {
|
||||
@@ -157,93 +149,17 @@ pub async fn prepare(options: PrepareOptions) -> Result<()> {
|
||||
mem_vfs.delete_db(filename);
|
||||
}
|
||||
|
||||
worker.db = Some(SQLiteDb::open(&worker.open_options.uri())?);
|
||||
worker.state = SQLiteState::Opened(SQLiteDb::open(&worker.open_options.uri())?);
|
||||
}
|
||||
|
||||
let stmts = worker
|
||||
.db
|
||||
.as_ref()
|
||||
.ok_or(WorkerError::InvaildState)?
|
||||
.prepare(&options.sql)?;
|
||||
worker.state = SQLiteState::Prepared(PreparedState {
|
||||
stmts,
|
||||
prepared: None,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn r#continue() -> Result<Vec<SQLiteStatementResult>> {
|
||||
with_worker(|worker| {
|
||||
let state = std::mem::replace(&mut worker.state, SQLiteState::Idie);
|
||||
let mut result = match state {
|
||||
SQLiteState::Idie => return Err(WorkerError::InvaildState),
|
||||
SQLiteState::Prepared(prepared_state) => {
|
||||
let mut result = vec![];
|
||||
if let Some(stmt) = prepared_state.prepared {
|
||||
result.push(stmt.pack(stmt.get_all()?));
|
||||
}
|
||||
result.extend(prepared_state.stmts.stmts_result()?);
|
||||
result
|
||||
}
|
||||
};
|
||||
result.push(SQLiteStatementResult::Finish);
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_over() -> Result<SQLiteStatementResult> {
|
||||
with_worker(|worker| match &mut worker.state {
|
||||
SQLiteState::Idie => Err(WorkerError::InvaildState),
|
||||
SQLiteState::Prepared(prepared_state) => {
|
||||
if let Some(prepared) = &mut prepared_state.prepared {
|
||||
if let Some(value) = prepared.get_one()? {
|
||||
Ok(prepared.pack(Some(value)))
|
||||
} else {
|
||||
let done = prepared.pack(None);
|
||||
prepared_state.prepared = None;
|
||||
Ok(done)
|
||||
}
|
||||
} else if let Some(prepared) = prepared_state.stmts.prepare_next()? {
|
||||
Ok(prepared.pack(prepared.get_all()?))
|
||||
} else {
|
||||
Ok(SQLiteStatementResult::Finish)
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_in() -> Result<()> {
|
||||
with_worker(|worker| {
|
||||
match &mut worker.state {
|
||||
SQLiteState::Idie => return Err(WorkerError::InvaildState),
|
||||
SQLiteState::Prepared(prepared_state) => {
|
||||
if prepared_state.prepared.is_some() {
|
||||
return Err(WorkerError::InvaildState);
|
||||
}
|
||||
let prepared = prepared_state
|
||||
.stmts
|
||||
.prepare_next()?
|
||||
.ok_or(WorkerError::InvaildState)?;
|
||||
prepared_state.prepared = Some(prepared);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_out() -> Result<SQLiteStatementResult> {
|
||||
with_worker(|worker| match &mut worker.state {
|
||||
SQLiteState::Idie => Err(WorkerError::InvaildState),
|
||||
SQLiteState::Prepared(prepared_state) => {
|
||||
if let Some(prepared) = prepared_state.prepared.take() {
|
||||
Ok(prepared.pack(prepared.get_all()?))
|
||||
} else {
|
||||
Err(WorkerError::InvaildState)
|
||||
match &worker.state {
|
||||
SQLiteState::NotOpened => Err(WorkerError::InvaildState),
|
||||
SQLiteState::Opened(sqlite_db) => {
|
||||
let stmts = sqlite_db.prepare(&options.sql)?;
|
||||
let result = stmts.stmts_result()?;
|
||||
Ok(SQLiteRunResult {
|
||||
embed: options.embed,
|
||||
result,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use sqlite_wasm_rs::*;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
|
||||
use crate::{
|
||||
InnerError, SQLiteStatementResult, SQLiteStatementTable, SQLiteStatementValues, SQLitendError,
|
||||
@@ -108,7 +107,6 @@ impl SQLiteStatements {
|
||||
|
||||
Ok(Some(SQLitePreparedStatement {
|
||||
sql,
|
||||
done: AtomicBool::new(false),
|
||||
position,
|
||||
sqlite3,
|
||||
stmt,
|
||||
@@ -121,6 +119,7 @@ impl SQLiteStatements {
|
||||
let stmt = stmt?;
|
||||
result.push(stmt.pack(stmt.get_all()?));
|
||||
}
|
||||
result.push(SQLiteStatementResult::Finish);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -136,7 +135,6 @@ impl Iterator for SQLiteStatements {
|
||||
pub struct SQLitePreparedStatement {
|
||||
sql: String,
|
||||
position: [usize; 2],
|
||||
done: AtomicBool,
|
||||
sqlite3: *mut sqlite3,
|
||||
stmt: *mut sqlite3_stmt,
|
||||
}
|
||||
@@ -149,10 +147,7 @@ impl SQLitePreparedStatement {
|
||||
fn step(&self) -> Result<bool> {
|
||||
let ret = unsafe { sqlite3_step(self.stmt) };
|
||||
match ret {
|
||||
SQLITE_DONE => {
|
||||
self.done.store(true, atomic::Ordering::SeqCst);
|
||||
Ok(false)
|
||||
}
|
||||
SQLITE_DONE => Ok(false),
|
||||
SQLITE_ROW => Ok(true),
|
||||
code => Err(SQLitendError::Step(sqlite_err(code, self.sqlite3))),
|
||||
}
|
||||
@@ -162,7 +157,6 @@ impl SQLitePreparedStatement {
|
||||
let values = SQLiteStatementTable {
|
||||
sql: self.sql.clone(),
|
||||
position: self.position,
|
||||
done: self.done.load(atomic::Ordering::SeqCst),
|
||||
values,
|
||||
};
|
||||
SQLiteStatementResult::Step(values)
|
||||
|
||||
Reference in New Issue
Block a user