diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js
index 280e57f5..609a673c 100644
--- a/packages/api/src/controllers/jsldata.js
+++ b/packages/api/src/controllers/jsldata.js
@@ -111,18 +111,22 @@ module.exports = {
getInfo_meta: true,
async getInfo({ jslid }) {
const file = getJslFileName(jslid);
- const firstLine = await readFirstLine(file);
- if (firstLine) {
- const parsed = JSON.parse(firstLine);
- if (parsed.__isStreamHeader) {
- return parsed;
+ try {
+ const firstLine = await readFirstLine(file);
+ if (firstLine) {
+ const parsed = JSON.parse(firstLine);
+ if (parsed.__isStreamHeader) {
+ return parsed;
+ }
+ return {
+ __isStreamHeader: true,
+ __isDynamicStructure: true,
+ };
}
- return {
- __isStreamHeader: true,
- __isDynamicStructure: true,
- };
+ return null;
+ } catch (err) {
+ return null;
}
- return null;
},
getRows_meta: true,
diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js
index b651c37c..f8e995fd 100644
--- a/packages/api/src/controllers/sessions.js
+++ b/packages/api/src/controllers/sessions.js
@@ -4,8 +4,10 @@ const connections = require('./connections');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const jsldata = require('./jsldata');
+const path = require('path');
const { handleProcessCommunication } = require('../utility/processComm');
const processArgs = require('../utility/processArgs');
+const { appdir } = require('../utility/directories');
module.exports = {
/** @type {import('dbgate-types').OpenedSession[]} */
@@ -46,9 +48,15 @@ module.exports = {
this.dispatchMessage(sesid, info);
},
- handle_done(sesid) {
+ handle_done(sesid, props) {
socket.emit(`session-done-${sesid}`);
- this.dispatchMessage(sesid, 'Query execution finished');
+ if (!props.skipFinishedMessage) {
+ this.dispatchMessage(sesid, 'Query execution finished');
+ }
+ const session = this.opened.find(x => x.sesid == sesid);
+ if (session.killOnDone) {
+ this.kill({ sesid });
+ }
},
handle_recordset(sesid, props) {
@@ -60,6 +68,11 @@ module.exports = {
jsldata.notifyChangedStats(stats);
},
+ handle_initializeFile(sesid, props) {
+ const { jslid } = props;
+ socket.emit(`session-initialize-file-${jslid}`);
+ },
+
handle_ping() {},
create_meta: true,
@@ -105,6 +118,19 @@ module.exports = {
return { state: 'ok' };
},
+ executeReader_meta: true,
+ async executeReader({ conid, database, sql, queryName, appFolder }) {
+ const { sesid } = await this.create({ conid, database });
+ const session = this.opened.find(x => x.sesid == sesid);
+ session.killOnDone = true;
+ const jslid = uuidv1();
+ const fileName = queryName && appFolder ? path.join(appdir(), appFolder, `${queryName}.query.sql`) : null;
+
+ session.subprocess.send({ msgtype: 'executeReader', sql, fileName, jslid });
+
+ return { jslid };
+ },
+
// cancel_meta: true,
// async cancel({ sesid }) {
// const session = this.opened.find((x) => x.sesid == sesid);
diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js
index 6e8355de..c63b8cdb 100644
--- a/packages/api/src/proc/sessionProcess.js
+++ b/packages/api/src/proc/sessionProcess.js
@@ -17,11 +17,15 @@ let afterConnectCallbacks = [];
// let currentHandlers = [];
class TableWriter {
- constructor(structure, resultIndex) {
- this.jslid = uuidv1();
- this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
+ constructor() {
this.currentRowCount = 0;
this.currentChangeIndex = 1;
+ this.initializedFile = false;
+ }
+
+ initializeFromQuery(structure, resultIndex) {
+ this.jslid = uuidv1();
+ this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
fs.writeFileSync(
this.currentFile,
JSON.stringify({
@@ -32,13 +36,21 @@ class TableWriter {
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
this.writeCurrentStats(false, false);
this.resultIndex = resultIndex;
+ this.initializedFile = true;
process.send({ msgtype: 'recordset', jslid: this.jslid, resultIndex });
}
+ initializeFromReader(jslid) {
+ this.jslid = jslid;
+ this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
+ this.writeCurrentStats(false, false);
+ }
+
row(row) {
// console.log('ACCEPT ROW', row);
this.currentStream.write(JSON.stringify(row) + '\n');
this.currentRowCount += 1;
+
if (!this.plannedStats) {
this.plannedStats = true;
process.nextTick(() => {
@@ -49,6 +61,21 @@ class TableWriter {
}
}
+ rowFromReader(row) {
+ if (!this.initializedFile) {
+ process.send({ msgtype: 'initializeFile', jslid: this.jslid });
+ this.initializedFile = true;
+
+ fs.writeFileSync(this.currentFile, JSON.stringify(row) + '\n');
+ this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
+ this.writeCurrentStats(false, false);
+ this.initializedFile = true;
+ return;
+ }
+
+ this.row(row);
+ }
+
writeCurrentStats(isFinished = false, emitEvent = false) {
const stats = {
rowCount: this.currentRowCount,
@@ -63,10 +90,11 @@ class TableWriter {
}
}
- close() {
+ close(afterClose) {
if (this.currentStream) {
this.currentStream.end(() => {
this.writeCurrentStats(true, true);
+ if (afterClose) afterClose();
});
}
}
@@ -98,7 +126,11 @@ class StreamHandler {
recordset(columns) {
this.closeCurrentWriter();
- this.currentWriter = new TableWriter(Array.isArray(columns) ? { columns } : columns, this.resultIndexHolder.value);
+ this.currentWriter = new TableWriter();
+ this.currentWriter.initializeFromQuery(
+ Array.isArray(columns) ? { columns } : columns,
+ this.resultIndexHolder.value
+ );
this.resultIndexHolder.value += 1;
// this.writeCurrentStats();
@@ -110,7 +142,6 @@ class StreamHandler {
// }, 500);
}
row(row) {
- // console.log('ACCEPT ROW', row);
if (this.currentWriter) this.currentWriter.row(row);
else if (row.message) process.send({ msgtype: 'info', info: { message: row.message } });
// this.onRow(this.jslid);
@@ -135,20 +166,21 @@ function handleStream(driver, resultIndexHolder, sql) {
});
}
-function ensureExecuteCustomScript(driver) {
+function allowExecuteCustomScript(driver) {
if (driver.readOnlySessions) {
- return;
+ return true;
}
if (storedConnection.isReadOnly) {
- throw new Error('Connection is read only');
+ return false;
+ // throw new Error('Connection is read only');
}
+ return true;
}
async function handleConnect(connection) {
storedConnection = connection;
const driver = requireEngineDriver(storedConnection);
- ensureExecuteCustomScript(driver);
systemConnection = await connectUtility(driver, storedConnection);
for (const [resolve] of afterConnectCallbacks) {
resolve();
@@ -173,6 +205,19 @@ async function handleExecuteQuery({ sql }) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
+ if (!allowExecuteCustomScript(driver)) {
+ process.send({
+ msgtype: 'info',
+ info: {
+ message: 'Connection without read-only sessions is read only',
+ severity: 'error',
+ },
+ });
+ process.send({ msgtype: 'done', skipFinishedMessage: true });
+ return;
+ //process.send({ msgtype: 'error', error: e.message });
+ }
+
const resultIndexHolder = {
value: 0,
};
@@ -186,9 +231,39 @@ async function handleExecuteQuery({ sql }) {
process.send({ msgtype: 'done' });
}
+async function handleExecuteReader({ jslid, sql, fileName }) {
+ await waitConnected();
+
+ const driver = requireEngineDriver(storedConnection);
+
+ if (fileName) {
+ sql = fs.readFileSync(fileName, 'utf-8');
+ } else {
+ if (!allowExecuteCustomScript(driver)) {
+ process.send({ msgtype: 'done' });
+ return;
+ }
+ }
+
+ const writer = new TableWriter();
+ writer.initializeFromReader(jslid);
+
+ const reader = await driver.readQuery(systemConnection, sql);
+
+ reader.on('data', data => {
+ writer.rowFromReader(data);
+ });
+ reader.on('end', () => {
+ writer.close(() => {
+ process.send({ msgtype: 'done' });
+ });
+ });
+}
+
const messageHandlers = {
connect: handleConnect,
executeQuery: handleExecuteQuery,
+ executeReader: handleExecuteReader,
// cancel: handleCancel,
};
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 48dd47e3..6b0174db 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -19,6 +19,7 @@ export interface OpenedSession {
sesid: string;
conid: string;
database: string;
+ killOnDone?: boolean;
subprocess: ChildProcess;
}
diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte
index c7bb2c01..3237c18c 100644
--- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte
+++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte
@@ -10,6 +10,7 @@
matviews: 'img view',
procedures: 'img procedure',
functions: 'img function',
+ queries: 'img query-data',
};
const defaultTabs = {
@@ -17,6 +18,7 @@
collections: 'CollectionDataTab',
views: 'ViewDataTab',
matviews: 'ViewDataTab',
+ queries: 'QueryDataTab',
};
const menus = {
@@ -231,6 +233,13 @@
},
},
],
+ queries: [
+ {
+ label: 'Open data',
+ tab: 'QueryDataTab',
+ forceNewTab: true,
+ },
+ ],
procedures: [
{
label: 'Drop procedure',
diff --git a/packages/web/src/datagrid/JslDataGrid.svelte b/packages/web/src/datagrid/JslDataGrid.svelte
index 651c720a..9cb7db2a 100644
--- a/packages/web/src/datagrid/JslDataGrid.svelte
+++ b/packages/web/src/datagrid/JslDataGrid.svelte
@@ -1,22 +1,42 @@
+
+
+
+
+ {#if jslid}
+
+ {:else}
+
+ {/if}
+
+
+
+
+
diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte
index dc11c89e..1fe3af03 100644
--- a/packages/web/src/tabs/QueryTab.svelte
+++ b/packages/web/src/tabs/QueryTab.svelte
@@ -140,7 +140,7 @@
}
export function hasConnection() {
- return !!conid && (!$connection.isReadOnly || driver.readOnlySessions);
+ return !!conid && (!$connection?.isReadOnly || driver?.readOnlySessions);
}
async function executeCore(sql) {
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index 18d6fafc..eabdc6a5 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -22,6 +22,7 @@ import * as JsonTab from './JsonTab.svelte';
import * as ChangelogTab from './ChangelogTab.svelte';
import * as DiagramTab from './DiagramTab.svelte';
import * as DbKeyDetailTab from './DbKeyDetailTab.svelte';
+import * as QueryDataTab from './QueryDataTab.svelte';
export default {
TableDataTab,
@@ -48,4 +49,5 @@ export default {
ChangelogTab,
DiagramTab,
DbKeyDetailTab,
+ QueryDataTab,
};
diff --git a/packages/web/src/widgets/AppFilesList.svelte b/packages/web/src/widgets/AppFilesList.svelte
index 3c50951a..2747ba49 100644
--- a/packages/web/src/widgets/AppFilesList.svelte
+++ b/packages/web/src/widgets/AppFilesList.svelte
@@ -79,12 +79,16 @@
text: 'New SQL command',
onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE),
},
+ {
+ text: 'New SQL query',
+ onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE),
+ },
{
text: 'New virtual references file',
onClick: () => handleNewConfigFile('virtual-references.config.json', []),
},
{
- text: 'New disctionary descriptions file',
+ text: 'New dictionary descriptions file',
onClick: () => handleNewConfigFile('dictionary-descriptions.config.json', []),
},
diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte
index 74769630..a4d9c60a 100644
--- a/packages/web/src/widgets/SqlObjectList.svelte
+++ b/packages/web/src/widgets/SqlObjectList.svelte
@@ -16,7 +16,7 @@
import InlineButton from '../buttons/InlineButton.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
- import { useConnectionInfo, useDatabaseInfo, useDatabaseStatus } from '../utility/metadataLoaders';
+ import { useConnectionInfo, useDatabaseInfo, useDatabaseStatus, useUsedApps } from '../utility/metadataLoaders';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import AppObjectList from '../appobj/AppObjectList.svelte';
import _ from 'lodash';
@@ -30,10 +30,11 @@
import FontIcon from '../icons/FontIcon.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { findEngineDriver } from 'dbgate-tools';
- import { extensions } from '../stores';
+ import { currentDatabase, extensions } from '../stores';
import newQuery from '../query/newQuery';
import runCommand from '../commands/runCommand';
import { apiCall } from '../utility/api';
+ import { filterAppsForDatabase } from '../utility/appTools';
export let conid;
export let database;
@@ -46,16 +47,28 @@
$: connection = useConnectionInfo({ conid });
$: driver = findEngineDriver($connection, $extensions);
+ $: apps = useUsedApps();
+
+ $: dbApps = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $apps || []);
+
// $: console.log('OBJECTS', $objects);
- $: objectList = _.flatten(
- ['tables', 'collections', 'views', 'matviews', 'procedures', 'functions'].map(objectTypeField =>
+ $: objectList = _.flatten([
+ ...['tables', 'collections', 'views', 'matviews', 'procedures', 'functions'].map(objectTypeField =>
_.sortBy(
(($objects || {})[objectTypeField] || []).map(obj => ({ ...obj, objectTypeField })),
['schemaName', 'pureName']
)
- )
- );
+ ),
+ ...dbApps.map(app =>
+ app.queries.map(query => ({
+ objectTypeField: 'queries',
+ pureName: query.name,
+ schemaName: app.name,
+ sql: query.sql
+ }))
+ ),
+ ]);
// let generateIndex = 0;
// setInterval(() => (generateIndex += 1), 2000);
@@ -69,7 +82,7 @@
const res = [];
if (driver?.databaseEngineTypes?.includes('document')) {
res.push({ command: 'new.collection' });
- }
+ }
if (driver?.databaseEngineTypes?.includes('sql')) {
res.push({ command: 'new.table' });
}