Merge branch 'dbgate:master' into oracle

This commit is contained in:
Rinie Kervel
2022-10-30 08:13:51 +01:00
committed by GitHub
86 changed files with 1927 additions and 480 deletions

View File

@@ -1,58 +0,0 @@
name: Docker image BETA
# on: [push]
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-docker.[0-9]+'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04]
steps:
- name: Context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: yarn install
run: |
# yarn --version
# yarn config set network-timeout 300000
yarn install
- name: setCurrentVersion
run: |
yarn setCurrentVersion
- name: Prepare docker image
run: |
yarn run prepare:docker
- name: Build docker image
run: |
docker build ./docker -t dbgate
- name: Push docker image
run: |
docker tag dbgate dbgate/dbgate:beta
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker push dbgate/dbgate:beta
- name: Build alpine docker image
run: |
docker build ./docker -t dbgate -f docker/Dockerfile-alpine
- name: Push alpine docker image
run: |
docker tag dbgate dbgate/dbgate:beta-alpine
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker push dbgate/dbgate:beta-alpine

View File

@@ -1,17 +1,11 @@
name: Docker image name: Docker image
# on: [push]
on: on:
push: push:
tags: tags:
- 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+'
# - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-docker.[0-9]+'
# on:
# push:
# branches:
# - production
jobs: jobs:
build: build:
@@ -30,12 +24,43 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
dbgate/dbgate
flavor: |
latest=false
tags: |
type=raw,value=beta,enable=${{ contains(github.ref_name, '-docker.') || contains(github.ref_name, '-beta.') }}
type=match,pattern=\d+.\d+.\d+,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
- name: Docker alpine meta
id: alpmeta
uses: docker/metadata-action@v4
with:
images: |
dbgate/dbgate
flavor: |
latest=false
tags: |
type=raw,value=beta-alpine,enable=${{ contains(github.ref_name, '-docker.') || contains(github.ref_name, '-beta.') }}
type=match,pattern=\d+.\d+.\d+,suffix=-alpine,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
type=raw,value=alpine,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }}
- name: Use Node.js 14.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
- name: yarn install - name: yarn install
run: | run: |
# yarn --version
# yarn config set network-timeout 300000
yarn install yarn install
- name: setCurrentVersion - name: setCurrentVersion
run: | run: |
@@ -43,19 +68,28 @@ jobs:
- name: Prepare docker image - name: Prepare docker image
run: | run: |
yarn run prepare:docker yarn run prepare:docker
- name: Build docker image
run: | - name: Set up Docker Buildx
docker build ./docker -t dbgate uses: docker/setup-buildx-action@v2
- name: Push docker image - name: Login to DockerHub
run: | uses: docker/login-action@v2
docker tag dbgate dbgate/dbgate with:
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} username: ${{ secrets.DOCKER_USERNAME }}
docker push dbgate/dbgate password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build alpine docker image
run: | - name: Build and push
docker build ./docker -t dbgate -f docker/Dockerfile-alpine uses: docker/build-push-action@v3
- name: Push alpine docker image with:
run: | push: true
docker tag dbgate dbgate/dbgate:alpine context: ./docker
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} tags: ${{ steps.meta.outputs.tags }}
docker push dbgate/dbgate:alpine platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: Build and push alpine
uses: docker/build-push-action@v3
with:
push: true
context: ./docker
file: ./docker/Dockerfile-alpine
tags: ${{ steps.alpmeta.outputs.tags }}
platforms: linux/amd64,linux/arm64,linux/arm/v7

View File

@@ -8,6 +8,32 @@ Builds:
- linux - application for linux - linux - application for linux
- win - application for Windows - win - application for Windows
### 5.1.5
- ADDED: Support perspectives for MongoDB - MongoDB query designer
- ADDED: Show JSON content directly in the overview #395
- CHANGED: OSX Command H shortcut for hiding window #390
- ADDED: Uppercase Autocomplete Suggestions #389
- FIXED: Record view left/right arrows cause start record number to be treated as string #388
- FIXED: MongoDb ObjectId behaviour not consistent in nested objects #387
- FIXED: demo.dbgate.org - beta version crash 5.1.5-beta.3 #386
- ADDED: connect via socket - configurable via environment variables #358
### 5.1.4
- ADDED: Drop database commands #384
- ADDED: Customizable Redis key separator #379
- ADDED: ARM support for docker images
- ADDED: Version tags for docker images
- ADDED: Better SQL command splitting and highlighting
- ADDED: Unsaved marker for SQL files
### 5.1.3
- ADDED: Editing multiline cell values #378 #371 #359
- ADDED: Truncate table #333
- ADDED: Perspectives - show row count
- ADDED: Query - error markers in gutter area
- ADDED: Query - ability to execute query elements from gutter
- FIXED: Correct error line numbers returned from queries
### 5.1.2 ### 5.1.2
- FIXED: MongoDb any export function does not work. #373 - FIXED: MongoDb any export function does not work. #373
- ADDED: Query Designer short order more flexibility #372 - ADDED: Query Designer short order more flexibility #372

14
misc/play-dark-mode.svg Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 17.804 17.804" style="enable-background:new 0 0 17.804 17.804;" xml:space="preserve">
<g>
<g id="c98_play">
<path fill='#ccc' d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313
c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04
c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z"/>
</g>
<g id="Capa_1_78_">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 733 B

14
misc/play-light-mode.svg Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 17.804 17.804" style="enable-background:new 0 0 17.804 17.804;" xml:space="preserve">
<g>
<g id="c98_play">
<path fill='#444' d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313
c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04
c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z"/>
</g>
<g id="Capa_1_78_">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "5.1.3-beta.1", "version": "5.1.5",
"name": "dbgate-all", "name": "dbgate-all",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",

View File

@@ -25,7 +25,7 @@
"compare-versions": "^3.6.0", "compare-versions": "^3.6.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"dbgate-sqltree": "^5.0.0-alpha.1", "dbgate-sqltree": "^5.0.0-alpha.1",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"debug": "^4.3.4", "debug": "^4.3.4",

View File

@@ -53,6 +53,8 @@ function getPortalCollections() {
databaseUrl: process.env[`URL_${id}`], databaseUrl: process.env[`URL_${id}`],
useDatabaseUrl: !!process.env[`URL_${id}`], useDatabaseUrl: !!process.env[`URL_${id}`],
databaseFile: process.env[`FILE_${id}`], databaseFile: process.env[`FILE_${id}`],
socketPath: process.env[`SOCKET_PATH_${id}`],
authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
defaultDatabase: defaultDatabase:
process.env[`DATABASE_${id}`] || process.env[`DATABASE_${id}`] ||
(process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null), (process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null),

View File

@@ -152,4 +152,13 @@ module.exports = {
opened.subprocess.send({ msgtype: 'createDatabase', name }); opened.subprocess.send({ msgtype: 'createDatabase', name });
return { status: 'ok' }; return { status: 'ok' };
}, },
dropDatabase_meta: true,
async dropDatabase({ conid, name }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (opened.connection.isReadOnly) return false;
opened.subprocess.send({ msgtype: 'dropDatabase', name });
return { status: 'ok' };
},
}; };

View File

@@ -177,7 +177,7 @@ async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) {
const res = await driver.query(systemConnection, sql); const res = await driver.query(systemConnection, sql);
process.send({ msgtype: 'response', msgid, ...res }); process.send({ msgtype: 'response', msgid, ...res });
} catch (err) { } catch (err) {
process.send({ msgtype: 'response', msgid, errorMessage: err.message }); process.send({ msgtype: 'response', msgid, errorMessage: err.message || 'Error executing SQL script' });
} }
} }

View File

@@ -2,7 +2,6 @@ const stableStringify = require('json-stable-stringify');
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools'); const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
const childProcessChecker = require('../utility/childProcessChecker'); const childProcessChecker = require('../utility/childProcessChecker');
const requireEngineDriver = require('../utility/requireEngineDriver'); const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility'); const connectUtility = require('../utility/connectUtility');
const { handleProcessCommunication } = require('../utility/processComm'); const { handleProcessCommunication } = require('../utility/processComm');
@@ -81,14 +80,16 @@ function handlePing() {
lastPing = new Date().getTime(); lastPing = new Date().getTime();
} }
async function handleCreateDatabase({ name }) { async function handleDatabaseOp(op, { name }) {
const driver = requireEngineDriver(storedConnection); const driver = requireEngineDriver(storedConnection);
systemConnection = await connectUtility(driver, storedConnection, 'app'); systemConnection = await connectUtility(driver, storedConnection, 'app');
console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`); if (driver[op]) {
if (driver.createDatabase) { await driver[op](systemConnection, name);
await driver.createDatabase(systemConnection, name);
} else { } else {
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`); const dmp = driver.createDumper();
dmp[op](name);
console.log(`RUNNING SCRIPT: ${dmp.s}`);
await driver.query(systemConnection, dmp.s);
} }
await handleRefresh(); await handleRefresh();
} }
@@ -96,7 +97,8 @@ async function handleCreateDatabase({ name }) {
const messageHandlers = { const messageHandlers = {
connect: handleConnect, connect: handleConnect,
ping: handlePing, ping: handlePing,
createDatabase: handleCreateDatabase, createDatabase: props => handleDatabaseOp('createDatabase', props),
dropDatabase: props => handleDatabaseOp('dropDatabase', props),
}; };
async function handleMessage({ msgtype, ...other }) { async function handleMessage({ msgtype, ...other }) {

View File

@@ -101,8 +101,9 @@ class TableWriter {
} }
class StreamHandler { class StreamHandler {
constructor(resultIndexHolder, resolve) { constructor(resultIndexHolder, resolve, startLine) {
this.recordset = this.recordset.bind(this); this.recordset = this.recordset.bind(this);
this.startLine = startLine;
this.row = this.row.bind(this); this.row = this.row.bind(this);
// this.error = this.error.bind(this); // this.error = this.error.bind(this);
this.done = this.done.bind(this); this.done = this.done.bind(this);
@@ -155,14 +156,21 @@ class StreamHandler {
this.resolve(); this.resolve();
} }
info(info) { info(info) {
if (info && info.line != null) {
info = {
...info,
line: this.startLine + info.line,
};
}
process.send({ msgtype: 'info', info }); process.send({ msgtype: 'info', info });
} }
} }
function handleStream(driver, resultIndexHolder, sql) { function handleStream(driver, resultIndexHolder, sqlItem) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const handler = new StreamHandler(resultIndexHolder, resolve); const start = sqlItem.trimStart || sqlItem.start;
driver.stream(systemConnection, sql, handler); const handler = new StreamHandler(resultIndexHolder, resolve, start && start.line);
driver.stream(systemConnection, sqlItem.text, handler);
}); });
} }
@@ -221,7 +229,10 @@ async function handleExecuteQuery({ sql }) {
const resultIndexHolder = { const resultIndexHolder = {
value: 0, value: 0,
}; };
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) { for (const sqlItem of splitQuery(sql, {
...driver.getQuerySplitterOptions('stream'),
returnRichInfo: true,
})) {
await handleStream(driver, resultIndexHolder, sqlItem); await handleStream(driver, resultIndexHolder, sqlItem);
// const handler = new StreamHandler(resultIndex); // const handler = new StreamHandler(resultIndex);
// const stream = await driver.stream(systemConnection, sqlItem, handler); // const stream = await driver.stream(systemConnection, sqlItem, handler);

View File

@@ -47,7 +47,7 @@ async function importDatabase({ connection = undefined, systemConnection = undef
const downloadedFile = await download(inputFile); const downloadedFile = await download(inputFile);
const fileStream = fs.createReadStream(downloadedFile, 'utf-8'); const fileStream = fs.createReadStream(downloadedFile, 'utf-8');
const splittedStream = splitQueryStream(fileStream, driver.getQuerySplitterOptions()); const splittedStream = splitQueryStream(fileStream, driver.getQuerySplitterOptions('script'));
const importStream = new ImportStream(pool, driver); const importStream = new ImportStream(pool, driver);
// @ts-ignore // @ts-ignore
splittedStream.pipe(importStream); splittedStream.pipe(importStream);

View File

@@ -5,6 +5,7 @@ import _zip from 'lodash/zip';
import _difference from 'lodash/difference'; import _difference from 'lodash/difference';
import debug from 'debug'; import debug from 'debug';
import stableStringify from 'json-stable-stringify'; import stableStringify from 'json-stable-stringify';
import { PerspectiveDataPattern } from './PerspectiveDataPattern';
const dbg = debug('dbgate:PerspectiveCache'); const dbg = debug('dbgate:PerspectiveCache');
@@ -34,6 +35,7 @@ export class PerspectiveCacheTable {
pureName: string; pureName: string;
bindingColumns?: string[]; bindingColumns?: string[];
dataColumns: string[]; dataColumns: string[];
allColumns?: boolean;
loadedAll: boolean; loadedAll: boolean;
loadedRows: any[] = []; loadedRows: any[] = [];
bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {}; bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {};
@@ -86,14 +88,23 @@ export class PerspectiveCache {
constructor() {} constructor() {}
tables: { [tableKey: string]: PerspectiveCacheTable } = {}; tables: { [tableKey: string]: PerspectiveCacheTable } = {};
dataPatterns: PerspectiveDataPattern[] = [];
getTableCache(props: PerspectiveDataLoadProps) { getTableCache(props: PerspectiveDataLoadProps) {
const tableKey = stableStringify( const tableKey = stableStringify(
_pick(props, ['schemaName', 'pureName', 'bindingColumns', 'databaseConfig', 'orderBy', 'condition']) _pick(props, [
'schemaName',
'pureName',
'bindingColumns',
'databaseConfig',
'orderBy',
'sqlCondition',
'mongoCondition',
])
); );
let res = this.tables[tableKey]; let res = this.tables[tableKey];
if (res && _difference(props.dataColumns, res.dataColumns).length > 0) { if (res && _difference(props.dataColumns, res.dataColumns).length > 0 && !res.allColumns) {
dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns); dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns);
// we have incomplete cache // we have incomplete cache
@@ -113,5 +124,6 @@ export class PerspectiveCache {
clear() { clear() {
this.tables = {}; this.tables = {};
this.dataPatterns = [];
} }
} }

View File

@@ -7,6 +7,13 @@ import uuidv1 from 'uuid/v1';
// uncheckedColumns: string[]; // uncheckedColumns: string[];
// } // }
export type PerspectiveDatabaseEngineType = 'sqldb' | 'docdb';
export interface PerspectiveDatabaseConfig {
conid: string;
database: string;
}
export interface PerspectiveCustomJoinConfig { export interface PerspectiveCustomJoinConfig {
refNodeDesignerId: string; refNodeDesignerId: string;
referenceDesignerId: string; referenceDesignerId: string;

View File

@@ -1,19 +1,40 @@
import { Condition, Expression, Select } from 'dbgate-sqltree'; import { Condition, Expression, Select } from 'dbgate-sqltree';
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider'; import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
import debug from 'debug'; import debug from 'debug';
import _zipObject from 'lodash/zipObject';
import _mapValues from 'lodash/mapValues';
import _isArray from 'lodash/isArray';
import { safeJsonParse } from 'dbgate-tools';
function normalizeLoadedRow(row) {
return _mapValues(row, v => safeJsonParse(v) || v);
}
function normalizeResult(result) {
if (_isArray(result)) {
return result.map(normalizeLoadedRow);
}
if (result.errorMessage) {
return result;
}
return {
...result,
errorMessage: 'Unspecified error',
};
}
const dbg = debug('dbgate:PerspectiveDataLoader'); const dbg = debug('dbgate:PerspectiveDataLoader');
export class PerspectiveDataLoader { export class PerspectiveDataLoader {
constructor(public apiCall) {} constructor(public apiCall) {}
buildCondition(props: PerspectiveDataLoadProps): Condition { buildSqlCondition(props: PerspectiveDataLoadProps): Condition {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props; const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, sqlCondition } = props;
const conditions = []; const conditions = [];
if (condition) { if (sqlCondition) {
conditions.push(condition); conditions.push(sqlCondition);
} }
if (bindingColumns?.length == 1) { if (bindingColumns?.length == 1) {
@@ -38,8 +59,26 @@ export class PerspectiveDataLoader {
: null; : null;
} }
async loadGrouping(props: PerspectiveDataLoadProps) { buildMongoCondition(props: PerspectiveDataLoadProps): {} {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns } = props; const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, mongoCondition } = props;
const conditions = [];
if (mongoCondition) {
conditions.push(mongoCondition);
}
if (bindingColumns?.length == 1) {
conditions.push({
[bindingColumns[0]]: { $in: bindingValues.map(x => x[0]) },
});
}
return conditions.length == 1 ? conditions[0] : conditions.length > 0 ? { $and: conditions } : null;
}
async loadGroupingSqlDb(props: PerspectiveDataLoadProps) {
const { schemaName, pureName, bindingColumns } = props;
const bindingColumnExpressions = bindingColumns.map( const bindingColumnExpressions = bindingColumns.map(
columnName => columnName =>
@@ -71,13 +110,13 @@ export class PerspectiveDataLoader {
}, },
...bindingColumnExpressions, ...bindingColumnExpressions,
], ],
where: this.buildCondition(props), where: this.buildSqlCondition(props),
}; };
select.groupBy = bindingColumnExpressions; select.groupBy = bindingColumnExpressions;
if (dbg?.enabled) { if (dbg?.enabled) {
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${props.dataColumns?.join(',')}`); dbg(`LOAD COUNTS, table=${props.pureName}, columns=${bindingColumns?.join(',')}`);
} }
const response = await this.apiCall('database-connections/sql-select', { const response = await this.apiCall('database-connections/sql-select', {
@@ -93,8 +132,63 @@ export class PerspectiveDataLoader {
})); }));
} }
async loadData(props: PerspectiveDataLoadProps) { async loadGroupingDocDb(props: PerspectiveDataLoadProps) {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props; const { schemaName, pureName, bindingColumns } = props;
const aggregate = [
{ $match: this.buildMongoCondition(props) },
{
$group: {
_id: _zipObject(
bindingColumns,
bindingColumns.map(col => '$' + col)
),
count: { $sum: 1 },
},
},
];
if (dbg?.enabled) {
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${bindingColumns?.join(',')}`);
}
const response = await this.apiCall('database-connections/collection-data', {
conid: props.databaseConfig.conid,
database: props.databaseConfig.database,
options: {
pureName,
aggregate,
},
});
if (response.errorMessage) return response;
return response.rows.map(row => ({
...row._id,
_perspective_group_size_: parseInt(row.count),
}));
}
async loadGrouping(props: PerspectiveDataLoadProps) {
const { engineType } = props;
switch (engineType) {
case 'sqldb':
return this.loadGroupingSqlDb(props);
case 'docdb':
return this.loadGroupingDocDb(props);
}
}
async loadDataSqlDb(props: PerspectiveDataLoadProps) {
const {
schemaName,
pureName,
bindingColumns,
bindingValues,
dataColumns,
orderBy,
sqlCondition: condition,
engineType,
} = props;
if (dataColumns?.length == 0) { if (dataColumns?.length == 0) {
return []; return [];
@@ -113,16 +207,19 @@ export class PerspectiveDataLoader {
}, },
})), })),
selectAll: !dataColumns, selectAll: !dataColumns,
orderBy: orderBy?.map(({ columnName, order }) => ({ orderBy:
orderBy?.length > 0
? orderBy?.map(({ columnName, order }) => ({
exprType: 'column', exprType: 'column',
columnName, columnName,
direction: order, direction: order,
source: { source: {
name: { schemaName, pureName }, name: { schemaName, pureName },
}, },
})), }))
: null,
range: props.range, range: props.range,
where: this.buildCondition(props), where: this.buildSqlCondition(props),
}; };
if (dbg?.enabled) { if (dbg?.enabled) {
@@ -143,8 +240,76 @@ export class PerspectiveDataLoader {
return response.rows; return response.rows;
} }
async loadRowCount(props: PerspectiveDataLoadProps) { getDocDbLoadOptions(props: PerspectiveDataLoadProps, useSort: boolean) {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props; const { pureName } = props;
const res: any = {
pureName,
condition: this.buildMongoCondition(props),
skip: props.range?.offset,
limit: props.range?.limit,
};
if (useSort && props.orderBy?.length > 0) {
res.sort = _zipObject(
props.orderBy.map(col => col.columnName),
props.orderBy.map(col => (col.order == 'DESC' ? -1 : 1))
);
}
return res;
}
async loadDataDocDb(props: PerspectiveDataLoadProps) {
const {
schemaName,
pureName,
bindingColumns,
bindingValues,
dataColumns,
orderBy,
sqlCondition: condition,
engineType,
} = props;
if (dbg?.enabled) {
dbg(
`LOAD DATA, collection=${props.pureName}, columns=${props.dataColumns?.join(',')}, range=${
props.range?.offset
},${props.range?.limit}`
);
}
const options = this.getDocDbLoadOptions(props, true);
const response = await this.apiCall('database-connections/collection-data', {
conid: props.databaseConfig.conid,
database: props.databaseConfig.database,
options,
});
if (response.errorMessage) return response;
return response.rows;
}
async loadData(props: PerspectiveDataLoadProps) {
const { engineType } = props;
switch (engineType) {
case 'sqldb':
return normalizeResult(await this.loadDataSqlDb(props));
case 'docdb':
return normalizeResult(await this.loadDataDocDb(props));
}
}
async loadRowCountSqlDb(props: PerspectiveDataLoadProps) {
const {
schemaName,
pureName,
bindingColumns,
bindingValues,
dataColumns,
orderBy,
sqlCondition: condition,
} = props;
const select: Select = { const select: Select = {
commandType: 'select', commandType: 'select',
@@ -158,7 +323,7 @@ export class PerspectiveDataLoader {
alias: 'count', alias: 'count',
}, },
], ],
where: this.buildCondition(props), where: this.buildSqlCondition(props),
}; };
const response = await this.apiCall('database-connections/sql-select', { const response = await this.apiCall('database-connections/sql-select', {
@@ -170,4 +335,39 @@ export class PerspectiveDataLoader {
if (response.errorMessage) return response; if (response.errorMessage) return response;
return response.rows[0]; return response.rows[0];
} }
async loadRowCountDocDb(props: PerspectiveDataLoadProps) {
const {
schemaName,
pureName,
bindingColumns,
bindingValues,
dataColumns,
orderBy,
sqlCondition: condition,
} = props;
const options = {
...this.getDocDbLoadOptions(props, false),
countDocuments: true,
};
const response = await this.apiCall('database-connections/collection-data', {
conid: props.databaseConfig.conid,
database: props.databaseConfig.database,
options,
});
return response;
}
async loadRowCount(props: PerspectiveDataLoadProps) {
const { engineType } = props;
switch (engineType) {
case 'sqldb':
return this.loadRowCountSqlDb(props);
case 'docdb':
return this.loadRowCountDocDb(props);
}
}
} }

View File

@@ -0,0 +1,95 @@
import { PerspectiveDataLoader } from './PerspectiveDataLoader';
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
import _isString from 'lodash/isString';
import _isPlainObject from 'lodash/isPlainObject';
import _isNumber from 'lodash/isNumber';
import _isBoolean from 'lodash/isBoolean';
import _isArray from 'lodash/isArray';
import { safeJsonParse } from 'dbgate-tools';
export type PerspectiveDataPatternColumnType = 'null' | 'oid' | 'string' | 'number' | 'boolean' | 'json';
export interface PerspectiveDataPatternColumn {
name: string;
types: PerspectiveDataPatternColumnType[];
columns: PerspectiveDataPatternColumn[];
}
export interface PerspectiveDataPattern {
conid: string;
database: string;
schemaName?: string;
pureName: string;
columns: PerspectiveDataPatternColumn[];
}
export type PerspectiveDataPatternDict = { [designerId: string]: PerspectiveDataPattern };
function detectValueType(value): PerspectiveDataPatternColumnType {
if (_isString(value)) return 'string';
if (_isNumber(value)) return 'number';
if (_isBoolean(value)) return 'boolean';
if (value?.$oid) return 'oid';
if (_isPlainObject(value) || _isArray(value)) return 'json';
if (value == null) return 'null';
}
function addObjectToColumns(columns: PerspectiveDataPatternColumn[], row) {
if (_isPlainObject(row)) {
for (const key of Object.keys(row)) {
let column: PerspectiveDataPatternColumn = columns.find(x => x.name == key);
if (!column) {
column = {
name: key,
types: [],
columns: [],
};
columns.push(column);
}
const value = row[key];
const type = detectValueType(value);
if (!column.types.includes(type)) {
column.types.push(type);
}
if (_isPlainObject(value)) {
addObjectToColumns(column.columns, value);
}
if (_isArray(value)) {
for (const item of value) {
addObjectToColumns(column.columns, item);
}
}
if (_isString(value)) {
const json = safeJsonParse(value);
if (json && (_isPlainObject(json) || _isArray(json))) {
if (!column.types.includes('json')) {
column.types.push('json');
}
if (_isPlainObject(json)) {
addObjectToColumns(column.columns, json);
}
if (_isArray(json)) {
for (const item of json) {
addObjectToColumns(column.columns, item);
}
}
}
}
}
}
}
export function analyseDataPattern(
patternBase: Omit<PerspectiveDataPattern, 'columns'>,
rows: any[]
): PerspectiveDataPattern {
const res: PerspectiveDataPattern = {
...patternBase,
columns: [],
};
// console.log('ROWS', rows);
for (const row of rows) {
addObjectToColumns(res.columns, row);
}
return res;
}

View File

@@ -1,24 +1,21 @@
import debug from 'debug'; import debug from 'debug';
import { Condition } from 'dbgate-sqltree'; import { Condition } from 'dbgate-sqltree';
import { RangeDefinition } from 'dbgate-types'; import { RangeDefinition } from 'dbgate-types';
import { format } from 'path';
import { PerspectiveBindingGroup, PerspectiveCache } from './PerspectiveCache'; import { PerspectiveBindingGroup, PerspectiveCache } from './PerspectiveCache';
import { PerspectiveDataLoader } from './PerspectiveDataLoader'; import { PerspectiveDataLoader } from './PerspectiveDataLoader';
import { PerspectiveDataPatternDict } from './PerspectiveDataPattern';
import { PerspectiveDatabaseConfig, PerspectiveDatabaseEngineType } from './PerspectiveConfig';
export const PERSPECTIVE_PAGE_SIZE = 100; export const PERSPECTIVE_PAGE_SIZE = 100;
const dbg = debug('dbgate:PerspectiveDataProvider'); const dbg = debug('dbgate:PerspectiveDataProvider');
export interface PerspectiveDatabaseConfig {
conid: string;
database: string;
}
export interface PerspectiveDataLoadProps { export interface PerspectiveDataLoadProps {
databaseConfig: PerspectiveDatabaseConfig; databaseConfig: PerspectiveDatabaseConfig;
schemaName: string; schemaName?: string;
pureName: string; pureName: string;
dataColumns: string[]; dataColumns?: string[];
allColumns?: boolean;
orderBy: { orderBy: {
columnName: string; columnName: string;
order: 'ASC' | 'DESC'; order: 'ASC' | 'DESC';
@@ -27,11 +24,17 @@ export interface PerspectiveDataLoadProps {
bindingValues?: any[][]; bindingValues?: any[][];
range?: RangeDefinition; range?: RangeDefinition;
topCount?: number; topCount?: number;
condition?: Condition; sqlCondition?: Condition;
mongoCondition?: any;
engineType: PerspectiveDatabaseEngineType;
} }
export class PerspectiveDataProvider { export class PerspectiveDataProvider {
constructor(public cache: PerspectiveCache, public loader: PerspectiveDataLoader) {} constructor(
public cache: PerspectiveCache,
public loader: PerspectiveDataLoader,
public dataPatterns: PerspectiveDataPatternDict
) {}
async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> { async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
dbg('load data', props); dbg('load data', props);
// console.log('LOAD DATA', props); // console.log('LOAD DATA', props);
@@ -182,6 +185,7 @@ export class PerspectiveDataProvider {
// load missing rows // load missing rows
tableCache.dataColumns = props.dataColumns; tableCache.dataColumns = props.dataColumns;
tableCache.allColumns = props.allColumns;
const nextRows = await this.loader.loadData({ const nextRows = await this.loader.loadData({
...props, ...props,

View File

@@ -3,6 +3,8 @@ import _max from 'lodash/max';
import _range from 'lodash/max'; import _range from 'lodash/max';
import _fill from 'lodash/fill'; import _fill from 'lodash/fill';
import _findIndex from 'lodash/findIndex'; import _findIndex from 'lodash/findIndex';
import _isPlainObject from 'lodash/isPlainObject';
import _isArray from 'lodash/isArray';
import debug from 'debug'; import debug from 'debug';
const dbg = debug('dbgate:PerspectiveDisplay'); const dbg = debug('dbgate:PerspectiveDisplay');
@@ -126,14 +128,14 @@ export class PerspectiveDisplay {
fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) { fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) {
for (const child of children) { for (const child of children) {
if (child.isCheckedColumn || child.isCheckedNode) { if (child.generatesHiearchicGridColumn || child.generatesDataGridColumn) {
this.processColumn(child, parentNodes); this.processColumn(child, parentNodes);
} }
} }
} }
processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) { processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) {
if (node.isCheckedColumn) { if (node.generatesDataGridColumn) {
const column = new PerspectiveDisplayColumn(this); const column = new PerspectiveDisplayColumn(this);
column.title = node.columnTitle; column.title = node.columnTitle;
column.dataField = node.dataField; column.dataField = node.dataField;
@@ -145,7 +147,7 @@ export class PerspectiveDisplay {
this.columns.push(column); this.columns.push(column);
} }
if (node.isExpandable && node.isCheckedNode) { if (node.generatesHiearchicGridColumn) {
const countBefore = this.columns.length; const countBefore = this.columns.length;
this.fillColumns(node.childNodes, [...parentNodes, node]); this.fillColumns(node.childNodes, [...parentNodes, node]);
@@ -167,13 +169,30 @@ export class PerspectiveDisplay {
// return _findIndex(this.columns, x => x.dataNode.designerId == node.designerId); // return _findIndex(this.columns, x => x.dataNode.designerId == node.designerId);
// } // }
extractArray(value) {
if (_isArray(value)) return value;
if (_isPlainObject(value)) return [value];
return [];
}
collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] { collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] {
// console.log('********** COLLECT ROWS', sourceRows); // console.log('********** COLLECT ROWS', sourceRows);
const columnNodes = nodes.filter(x => x.isCheckedColumn); const columnNodes = nodes.filter(x => x.generatesDataGridColumn);
const treeNodes = nodes.filter(x => x.isCheckedNode); const treeNodes = nodes.filter(x => x.generatesHiearchicGridColumn);
// console.log('columnNodes', columnNodes); // console.log(
// console.log('treeNodes', treeNodes); // 'columnNodes',
// columnNodes.map(x => x.title)
// );
// console.log(
// 'treeNodes',
// treeNodes.map(x => x.title)
// );
// console.log(
// 'nodes',
// nodes.map(x => x.title)
// );
const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node)); const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node));
@@ -181,13 +200,14 @@ export class PerspectiveDisplay {
for (const sourceRow of sourceRows) { for (const sourceRow of sourceRows) {
// console.log('PROCESS SOURCE', sourceRow); // console.log('PROCESS SOURCE', sourceRow);
// row.startIndex = startIndex; // row.startIndex = startIndex;
const rowData = columnNodes.map(node => sourceRow[node.codeName]); const rowData = columnNodes.map(node => sourceRow[node.columnName]);
const subRowCollections = []; const subRowCollections = [];
for (const node of treeNodes) { for (const node of treeNodes) {
// console.log('sourceRow[node.fieldName]', node.fieldName, sourceRow[node.fieldName]);
if (sourceRow[node.fieldName]) { if (sourceRow[node.fieldName]) {
const subrows = { const subrows = {
rows: this.collectRows(sourceRow[node.fieldName], node.childNodes), rows: this.collectRows(this.extractArray(sourceRow[node.fieldName]), node.childNodes),
}; };
subRowCollections.push(subrows); subRowCollections.push(subrows);
} }

View File

@@ -1,4 +1,5 @@
import { import {
CollectionInfo,
ColumnInfo, ColumnInfo,
DatabaseInfo, DatabaseInfo,
ForeignKeyInfo, ForeignKeyInfo,
@@ -7,13 +8,15 @@ import {
TableInfo, TableInfo,
ViewInfo, ViewInfo,
} from 'dbgate-types'; } from 'dbgate-types';
import { equalFullName } from 'dbgate-tools'; import { equalFullName, isCollectionInfo, isTableInfo, isViewInfo } from 'dbgate-tools';
import { import {
ChangePerspectiveConfigFunc, ChangePerspectiveConfigFunc,
createPerspectiveNodeConfig, createPerspectiveNodeConfig,
MultipleDatabaseInfo, MultipleDatabaseInfo,
PerspectiveConfig, PerspectiveConfig,
PerspectiveCustomJoinConfig, PerspectiveCustomJoinConfig,
PerspectiveDatabaseConfig,
PerspectiveDatabaseEngineType,
PerspectiveFilterColumnInfo, PerspectiveFilterColumnInfo,
PerspectiveNodeConfig, PerspectiveNodeConfig,
PerspectiveReferenceConfig, PerspectiveReferenceConfig,
@@ -27,17 +30,14 @@ import _uniqBy from 'lodash/uniqBy';
import _sortBy from 'lodash/sortBy'; import _sortBy from 'lodash/sortBy';
import _cloneDeepWith from 'lodash/cloneDeepWith'; import _cloneDeepWith from 'lodash/cloneDeepWith';
import _findIndex from 'lodash/findIndex'; import _findIndex from 'lodash/findIndex';
import { import { PerspectiveDataLoadProps, PerspectiveDataProvider } from './PerspectiveDataProvider';
PerspectiveDatabaseConfig,
PerspectiveDataLoadProps,
PerspectiveDataProvider,
} from './PerspectiveDataProvider';
import stableStringify from 'json-stable-stringify'; import stableStringify from 'json-stable-stringify';
import { getFilterType, parseFilter } from 'dbgate-filterparser'; import { getFilterType, parseFilter } from 'dbgate-filterparser';
import { FilterType } from 'dbgate-filterparser/lib/types'; import { FilterType } from 'dbgate-filterparser/lib/types';
import { Condition, Expression, Select } from 'dbgate-sqltree'; import { Condition, Expression, Select } from 'dbgate-sqltree';
// import { getPerspectiveDefaultColumns } from './getPerspectiveDefaultColumns'; // import { getPerspectiveDefaultColumns } from './getPerspectiveDefaultColumns';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import { PerspectiveDataPatternColumn } from './PerspectiveDataPattern';
export interface PerspectiveDataLoadPropsWithNode { export interface PerspectiveDataLoadPropsWithNode {
props: PerspectiveDataLoadProps; props: PerspectiveDataLoadProps;
@@ -79,7 +79,7 @@ export abstract class PerspectiveTreeNode {
this.parentNodeConfig = parentNode?.nodeConfig; this.parentNodeConfig = parentNode?.nodeConfig;
} }
readonly nodeConfig: PerspectiveNodeConfig; readonly nodeConfig: PerspectiveNodeConfig;
readonly parentNodeConfig: PerspectiveNodeConfig; parentNodeConfig: PerspectiveNodeConfig;
// defaultChecked: boolean; // defaultChecked: boolean;
abstract get title(); abstract get title();
abstract get codeName(); abstract get codeName();
@@ -108,6 +108,18 @@ export abstract class PerspectiveTreeNode {
get namedObject(): NamedObjectInfo { get namedObject(): NamedObjectInfo {
return null; return null;
} }
get tableNodeOrParent(): PerspectiveTableNode {
if (this instanceof PerspectiveTableNode) {
return this;
}
if (this.parentNode == null) {
return null;
}
return this.parentNode.tableNodeOrParent;
}
get engineType(): PerspectiveDatabaseEngineType {
return null;
}
abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps; abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps;
get isRoot() { get isRoot() {
return this.parentNode == null; return this.parentNode == null;
@@ -119,6 +131,12 @@ export abstract class PerspectiveTreeNode {
get isSortable() { get isSortable() {
return false; return false;
} }
get generatesHiearchicGridColumn() {
return this.isExpandable && this.isCheckedNode;
}
get generatesDataGridColumn() {
return this.isCheckedColumn;
}
matchChildRow(parentRow: any, childRow: any): boolean { matchChildRow(parentRow: any, childRow: any): boolean {
return true; return true;
} }
@@ -271,14 +289,15 @@ export abstract class PerspectiveTreeNode {
[field]: isIncluded ? [...(n[field] || []), this.codeName] : (n[field] || []).filter(x => x != this.codeName), [field]: isIncluded ? [...(n[field] || []), this.codeName] : (n[field] || []).filter(x => x != this.codeName),
}); });
const [cfgChanged, nodeCfg] = this.parentNode?.ensureNodeConfig(cfg); const [cfgChanged, nodeCfg] = this.parentNode?.tableNodeOrParent?.ensureNodeConfig(cfg);
return { const res = {
...cfgChanged, ...cfgChanged,
nodes: cfgChanged.nodes.map(n => nodes: cfgChanged.nodes.map(n =>
n.designerId == (this.parentNode?.designerId || nodeCfg?.designerId) ? changedFields(n) : n n.designerId == (this.parentNode?.tableNodeOrParent?.designerId || nodeCfg?.designerId) ? changedFields(n) : n
), ),
}; };
return res;
}); });
} }
@@ -292,11 +311,15 @@ export abstract class PerspectiveTreeNode {
...this.childNodes.map(x => x.childDataColumn), ...this.childNodes.map(x => x.childDataColumn),
..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())), ..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())),
...this.getParentMatchColumns(), ...this.getParentMatchColumns(),
...this.childNodes
.filter(x => x instanceof PerspectivePatternColumnNode)
.filter(x => this.nodeConfig?.checkedColumns?.find(y => y.startsWith(x.codeName + '::')))
.map(x => x.columnName),
]) ])
); );
} }
getChildrenCondition(source = null): Condition { getChildrenSqlCondition(source = null): Condition {
const conditions = _compact([ const conditions = _compact([
...this.childNodes.map(x => x.parseFilterCondition(source)), ...this.childNodes.map(x => x.parseFilterCondition(source)),
...this.buildParentFilterConditions(), ...this.buildParentFilterConditions(),
@@ -313,7 +336,18 @@ export abstract class PerspectiveTreeNode {
}; };
} }
getOrderBy(table: TableInfo | ViewInfo): PerspectiveDataLoadProps['orderBy'] { getChildrenMongoCondition(source = null): {} {
const conditions = _compact([...this.childNodes.map(x => x.parseFilterCondition(source))]);
if (conditions.length == 0) {
return null;
}
if (conditions.length == 1) {
return conditions[0];
}
return { $and: conditions };
}
getOrderBy(table: TableInfo | ViewInfo | CollectionInfo): PerspectiveDataLoadProps['orderBy'] {
const res = _compact( const res = _compact(
this.childNodes.map(node => { this.childNodes.map(node => {
const sort = this.nodeConfig?.sort?.find(x => x.columnName == node.columnName); const sort = this.nodeConfig?.sort?.find(x => x.columnName == node.columnName);
@@ -325,11 +359,15 @@ export abstract class PerspectiveTreeNode {
} }
}) })
); );
return res.length > 0 if (res.length > 0) return res;
? res const pkColumns = (table as TableInfo)?.primaryKey?.columns.map(x => ({
: (table as TableInfo)?.primaryKey?.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) || [ columnName: x.columnName,
{ columnName: table?.columns[0].columnName, order: 'ASC' }, order: 'ASC' as 'ASC',
]; }));
if (pkColumns) return pkColumns;
const columns = (table as TableInfo | ViewInfo)?.columns;
if (columns) return [{ columnName: columns[0].columnName, order: 'ASC' }];
return [{ columnName: '_id', order: 'ASC' }];
} }
getBaseTables() { getBaseTables() {
@@ -390,7 +428,9 @@ export abstract class PerspectiveTreeNode {
return ( return (
(this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) && (this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) &&
this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid && this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid &&
this.parentNode?.databaseConfig?.database == this.databaseConfig?.database this.parentNode?.databaseConfig?.database == this.databaseConfig?.database &&
this.engineType == 'sqldb' &&
this.parentNode?.engineType == 'sqldb'
); );
} }
@@ -438,7 +478,7 @@ export abstract class PerspectiveTreeNode {
conditionType: 'and', conditionType: 'and',
conditions: _compact([ conditions: _compact([
...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName), ...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName),
leafNode.getChildrenCondition({ alias: 'pert_0' }), leafNode.getChildrenSqlCondition({ alias: 'pert_0' }),
]), ]),
}; };
@@ -496,6 +536,10 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
); );
} }
get engineType() {
return this.parentNode.engineType;
}
matchChildRow(parentRow: any, childRow: any): boolean { matchChildRow(parentRow: any, childRow: any): boolean {
if (!this.foreignKey) return false; if (!this.foreignKey) return false;
return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName]; return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
@@ -552,7 +596,8 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
dataColumns: this.getDataLoadColumns(), dataColumns: this.getDataLoadColumns(),
databaseConfig: this.databaseConfig, databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.refTable), orderBy: this.getOrderBy(this.refTable),
condition: this.getChildrenCondition(), sqlCondition: this.getChildrenSqlCondition(),
engineType: 'sqldb',
}; };
} }
@@ -573,6 +618,7 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
get fieldName() { get fieldName() {
return this.codeName + 'Ref'; return this.codeName + 'Ref';
// return this.codeName ;
} }
get title() { get title() {
@@ -670,6 +716,7 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
pureName: this.foreignKey.refTableName, pureName: this.foreignKey.refTableName,
conid: this.databaseConfig.conid, conid: this.databaseConfig.conid,
database: this.databaseConfig.database, database: this.databaseConfig.database,
objectTypeField: this.table.objectTypeField,
}; };
} }
return null; return null;
@@ -693,9 +740,216 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
} }
} }
export class PerspectivePatternColumnNode extends PerspectiveTreeNode {
foreignKey: ForeignKeyInfo;
refTable: TableInfo;
constructor(
public table: TableInfo | ViewInfo | CollectionInfo,
public column: PerspectiveDataPatternColumn,
public tableColumn: ColumnInfo,
dbs: MultipleDatabaseInfo,
config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc,
dataProvider: PerspectiveDataProvider,
databaseConfig: PerspectiveDatabaseConfig,
parentNode: PerspectiveTreeNode,
designerId: string
) {
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId);
this.parentNodeConfig = this.tableNodeOrParent?.nodeConfig;
}
get isChildColumn() {
return this.parentNode instanceof PerspectivePatternColumnNode;
}
// matchChildRow(parentRow: any, childRow: any): boolean {
// if (!this.foreignKey) return false;
// return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
// }
// getChildMatchColumns() {
// if (!this.foreignKey) return [];
// return [this.foreignKey.columns[0].columnName];
// }
// getParentMatchColumns() {
// if (!this.foreignKey) return [];
// return [this.foreignKey.columns[0].refColumnName];
// }
// getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
// if (!this.foreignKey) return [];
// return this.foreignKey.columns.map(column => {
// const res: Condition = {
// conditionType: 'binary',
// operator: '=',
// left: {
// exprType: 'column',
// columnName: column.columnName,
// source: { alias: parentAlias },
// },
// right: {
// exprType: 'column',
// columnName: column.refColumnName,
// source: { alias },
// },
// };
// return res;
// });
// }
// createReferenceConfigColumns(): PerspectiveReferenceConfig['columns'] {
// return this.foreignKey?.columns?.map(col => ({
// source: col.columnName,
// target: col.refColumnName,
// }));
// }
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
return null;
}
get generatesHiearchicGridColumn() {
// console.log('generatesHiearchicGridColumn', this.parentTableNode?.nodeConfig?.checkedColumns, this.codeName + '::');
return !!this.tableNodeOrParent?.nodeConfig?.checkedColumns?.find(x => x.startsWith(this.codeName + '::'));
}
// get generatesHiearchicGridColumn() {
// // return this.config &&;
// }
get icon() {
if (this.column.types.includes('json')) {
return 'img json';
}
return 'img column';
}
get codeName() {
if (this.parentNode instanceof PerspectivePatternColumnNode) {
return `${this.parentNode.codeName}::${this.column.name}`;
}
return this.column.name;
}
get columnName() {
return this.column.name;
}
get fieldName() {
return this.column.name;
}
get title() {
return this.column.name;
}
get isExpandable() {
return this.column.columns.length > 0;
}
get isSortable() {
return !this.isChildColumn;
}
get filterType(): FilterType {
if (this.tableColumn) return getFilterType(this.tableColumn.dataType);
return 'mongo';
}
generateChildNodes(): PerspectiveTreeNode[] {
return this.column.columns.map(
column =>
new PerspectivePatternColumnNode(
this.table,
column,
this.tableColumn,
this.dbs,
this.config,
this.setConfig,
this.dataProvider,
this.databaseConfig,
this,
null
)
);
return [];
// if (!this.foreignKey) return [];
// const tbl = this?.db?.tables?.find(
// x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
// );
// return getTableChildPerspectiveNodes(
// tbl,
// this.dbs,
// this.config,
// this.setConfig,
// this.dataProvider,
// this.databaseConfig,
// this
// );
}
get filterInfo(): PerspectiveFilterColumnInfo {
if (this.isChildColumn) {
return null;
}
return {
columnName: this.columnName,
filterType: this.filterType,
pureName: this.table.pureName,
schemaName: this.table.schemaName,
foreignKey: this.foreignKey,
};
}
parseFilterCondition(source = null): {} {
const filter = this.getFilter();
if (!filter) return null;
const condition = parseFilter(filter, 'mongo');
if (!condition) return null;
return _cloneDeepWith(condition, expr => {
if (expr.__placeholder__) {
return {
[this.columnName]: expr.__placeholder__,
};
}
});
}
// get headerTableAttributes() {
// if (this.foreignKey) {
// return {
// schemaName: this.foreignKey.refSchemaName,
// pureName: this.foreignKey.refTableName,
// conid: this.databaseConfig.conid,
// database: this.databaseConfig.database,
// };
// }
// return null;
// }
// get tableCode() {
// return `${this.collection.schemaName}|${this.table.pureName}`;
// }
// get namedObject(): NamedObjectInfo {
// if (this.foreignKey) {
// return {
// schemaName: this.foreignKey.refSchemaName,
// pureName: this.foreignKey.refTableName,
// };
// }
// return null;
// }
}
export class PerspectiveTableNode extends PerspectiveTreeNode { export class PerspectiveTableNode extends PerspectiveTreeNode {
constructor( constructor(
public table: TableInfo | ViewInfo, public table: TableInfo | ViewInfo | CollectionInfo,
dbs: MultipleDatabaseInfo, dbs: MultipleDatabaseInfo,
config: PerspectiveConfig, config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc, setConfig: ChangePerspectiveConfigFunc,
@@ -707,14 +961,22 @@ export class PerspectiveTableNode extends PerspectiveTreeNode {
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId); super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId);
} }
get engineType(): PerspectiveDatabaseEngineType {
return isCollectionInfo(this.table) ? 'docdb' : 'sqldb';
}
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps { getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
const isMongo = isCollectionInfo(this.table);
return { return {
schemaName: this.table.schemaName, schemaName: this.table.schemaName,
pureName: this.table.pureName, pureName: this.table.pureName,
dataColumns: this.getDataLoadColumns(), dataColumns: this.getDataLoadColumns(),
allColumns: isMongo,
databaseConfig: this.databaseConfig, databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.table), orderBy: this.getOrderBy(this.table),
condition: this.getChildrenCondition(), sqlCondition: isMongo ? null : this.getChildrenSqlCondition(),
mongoCondition: isMongo ? this.getChildrenMongoCondition() : null,
engineType: isMongo ? 'docdb' : 'sqldb',
}; };
} }
@@ -756,6 +1018,7 @@ export class PerspectiveTableNode extends PerspectiveTreeNode {
pureName: this.table.pureName, pureName: this.table.pureName,
conid: this.databaseConfig.conid, conid: this.databaseConfig.conid,
database: this.databaseConfig.database, database: this.databaseConfig.database,
objectTypeField: this.table.objectTypeField,
}; };
} }
@@ -770,64 +1033,6 @@ export class PerspectiveTableNode extends PerspectiveTreeNode {
}; };
} }
} }
// export class PerspectiveViewNode extends PerspectiveTreeNode {
// constructor(
// public view: ViewInfo,
// dbs: MultipleDatabaseInfo,
// config: PerspectiveConfig,
// setConfig: ChangePerspectiveConfigFunc,
// public dataProvider: PerspectiveDataProvider,
// databaseConfig: PerspectiveDatabaseConfig,
// parentNode: PerspectiveTreeNode
// ) {
// super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
// }
// getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
// return {
// schemaName: this.view.schemaName,
// pureName: this.view.pureName,
// dataColumns: this.getDataLoadColumns(),
// databaseConfig: this.databaseConfig,
// orderBy: this.getOrderBy(this.view),
// condition: this.getChildrenCondition(),
// };
// }
// get codeName() {
// return this.view.schemaName ? `${this.view.schemaName}:${this.view.pureName}` : this.view.pureName;
// }
// get title() {
// return this.view.pureName;
// }
// get isExpandable() {
// return true;
// }
// get childNodes(): PerspectiveTreeNode[] {
// return getTableChildPerspectiveNodes(
// this.view,
// this.dbs,
// this.config,
// this.setConfig,
// this.dataProvider,
// this.databaseConfig,
// this
// );
// }
// get icon() {
// return 'img table';
// }
// getBaseTableFromThis() {
// return this.view;
// }
// }
export class PerspectiveTableReferenceNode extends PerspectiveTableNode { export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
constructor( constructor(
public foreignKey: ForeignKeyInfo, public foreignKey: ForeignKeyInfo,
@@ -872,7 +1077,8 @@ export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
dataColumns: this.getDataLoadColumns(), dataColumns: this.getDataLoadColumns(),
databaseConfig: this.databaseConfig, databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.table), orderBy: this.getOrderBy(this.table),
condition: this.getChildrenCondition(), sqlCondition: this.getChildrenSqlCondition(),
engineType: 'sqldb',
}; };
} }
@@ -934,7 +1140,7 @@ export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode { export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
constructor( constructor(
public customJoin: PerspectiveCustomJoinConfig, public customJoin: PerspectiveCustomJoinConfig,
table: TableInfo | ViewInfo, table: TableInfo | ViewInfo | CollectionInfo,
dbs: MultipleDatabaseInfo, dbs: MultipleDatabaseInfo,
config: PerspectiveConfig, config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc, setConfig: ChangePerspectiveConfigFunc,
@@ -966,6 +1172,8 @@ export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps { getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
// console.log('CUSTOM JOIN', this.customJoin); // console.log('CUSTOM JOIN', this.customJoin);
// console.log('this.getDataLoadColumns()', this.getDataLoadColumns()); // console.log('this.getDataLoadColumns()', this.getDataLoadColumns());
const isMongo = isCollectionInfo(this.table);
return { return {
schemaName: this.table.schemaName, schemaName: this.table.schemaName,
pureName: this.table.pureName, pureName: this.table.pureName,
@@ -975,9 +1183,12 @@ export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
stableStringify stableStringify
), ),
dataColumns: this.getDataLoadColumns(), dataColumns: this.getDataLoadColumns(),
allColumns: isMongo,
databaseConfig: this.databaseConfig, databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.table), orderBy: this.getOrderBy(this.table),
condition: this.getChildrenCondition(), sqlCondition: isMongo ? null : this.getChildrenSqlCondition(),
mongoCondition: isMongo ? this.getChildrenMongoCondition() : null,
engineType: isMongo ? 'docdb' : 'sqldb',
}; };
} }
@@ -1082,7 +1293,7 @@ function findDesignerIdForNode<T extends PerspectiveTreeNode>(
} }
export function getTableChildPerspectiveNodes( export function getTableChildPerspectiveNodes(
table: TableInfo | ViewInfo, table: TableInfo | ViewInfo | CollectionInfo,
dbs: MultipleDatabaseInfo, dbs: MultipleDatabaseInfo,
config: PerspectiveConfig, config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc, setConfig: ChangePerspectiveConfigFunc,
@@ -1093,14 +1304,29 @@ export function getTableChildPerspectiveNodes(
if (!table) return []; if (!table) return [];
const db = parentNode.db; const db = parentNode.db;
const columnNodes = table.columns.map(col => const pattern = dataProvider?.dataPatterns?.[parentNode.designerId];
findDesignerIdForNode(
config, const tableOrView = isTableInfo(table) || isViewInfo(table) ? table : null;
parentNode,
designerId => const columnNodes =
new PerspectiveTableColumnNode( tableOrView?.columns?.map(col =>
col, findDesignerIdForNode(config, parentNode, designerId =>
pattern?.columns?.find(x => x.name == col.columnName)?.types.includes('json')
? new PerspectivePatternColumnNode(
table, table,
pattern?.columns?.find(x => x.name == col.columnName),
col,
dbs,
config,
setConfig,
dataProvider,
databaseConfig,
parentNode,
designerId
)
: new PerspectiveTableColumnNode(
col,
tableOrView,
dbs, dbs,
config, config,
setConfig, setConfig,
@@ -1110,8 +1336,27 @@ export function getTableChildPerspectiveNodes(
designerId designerId
) )
) )
); ) ||
pattern?.columns?.map(col =>
findDesignerIdForNode(
config,
parentNode,
designerId =>
new PerspectivePatternColumnNode(
table,
col,
null,
dbs,
config,
setConfig,
dataProvider,
databaseConfig,
parentNode,
designerId
)
)
) ||
[];
// if (!columnNodes.find(x => x.isChecked)) { // if (!columnNodes.find(x => x.isChecked)) {
// const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName); // const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName);
// const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns); // const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns);
@@ -1173,6 +1418,7 @@ export function getTableChildPerspectiveNodes(
const db = dbs?.[newConfig.conid]?.[newConfig.database]; const db = dbs?.[newConfig.conid]?.[newConfig.database];
const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const collection = db?.collections?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const join: PerspectiveCustomJoinConfig = { const join: PerspectiveCustomJoinConfig = {
refNodeDesignerId: node.designerId, refNodeDesignerId: node.designerId,
@@ -1189,11 +1435,11 @@ export function getTableChildPerspectiveNodes(
: ref.columns.map(col => ({ baseColumnName: col.target, refColumnName: col.source })), : ref.columns.map(col => ({ baseColumnName: col.target, refColumnName: col.source })),
}; };
if (table || view) { if (table || view || collection) {
customs.push( customs.push(
new PerspectiveCustomJoinTreeNode( new PerspectiveCustomJoinTreeNode(
join, join,
table || view, table || view || collection,
dbs, dbs,
config, config,
setConfig, setConfig,
@@ -1210,34 +1456,5 @@ export function getTableChildPerspectiveNodes(
res.push(..._sortBy(customs, 'title')); res.push(..._sortBy(customs, 'title'));
// const customs = [];
// for (const join of config.customJoins || []) {
// if (join.baseUniqueName == parentColumn.uniqueName) {
// const newConfig = { ...databaseConfig };
// if (join.conid) newConfig.conid = join.conid;
// if (join.database) newConfig.database = join.database;
// const db = dbs?.[newConfig.conid]?.[newConfig.database];
// const table = db?.tables?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
// const view = db?.views?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
// if (table || view) {
// customs.push(
// new PerspectiveCustomJoinTreeNode(
// join,
// table || view,
// dbs,
// config,
// setConfig,
// dataProvider,
// newConfig,
// parentColumn,
// null
// )
// );
// }
// }
// }
// res.push(..._sortBy(customs, 'title'));
return res; return res;
} }

View File

@@ -19,3 +19,4 @@ export * from './PerspectiveDataProvider';
export * from './PerspectiveCache'; export * from './PerspectiveCache';
export * from './PerspectiveConfig'; export * from './PerspectiveConfig';
export * from './processPerspectiveDefaultColunns'; export * from './processPerspectiveDefaultColunns';
export * from './PerspectiveDataPattern';

View File

@@ -1,8 +1,17 @@
import { findForeignKeyForColumn } from 'dbgate-tools'; import { findForeignKeyForColumn } from 'dbgate-tools';
import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types'; import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types';
import { createPerspectiveNodeConfig, MultipleDatabaseInfo, PerspectiveConfig } from './PerspectiveConfig'; import { createPerspectiveNodeConfig, MultipleDatabaseInfo, PerspectiveConfig } from './PerspectiveConfig';
import { PerspectiveDataPattern, PerspectiveDataPatternDict } from './PerspectiveDataPattern';
import { PerspectiveTableNode } from './PerspectiveTreeNode'; import { PerspectiveTableNode } from './PerspectiveTreeNode';
const namePredicates = [
x => x.toLowerCase() == 'name',
x => x.toLowerCase() == 'title',
x => x.toLowerCase().includes('name'),
x => x.toLowerCase().includes('title'),
x => x.toLowerCase().includes('subject'),
];
function getPerspectiveDefaultColumns( function getPerspectiveDefaultColumns(
table: TableInfo | ViewInfo, table: TableInfo | ViewInfo,
db: DatabaseInfo, db: DatabaseInfo,
@@ -10,13 +19,7 @@ function getPerspectiveDefaultColumns(
): [string[], string[]] { ): [string[], string[]] {
const columns = table.columns.map(x => x.columnName); const columns = table.columns.map(x => x.columnName);
const predicates = [ const predicates = [
x => x.toLowerCase() == 'name', ...namePredicates,
x => x.toLowerCase() == 'title',
x => x.toLowerCase().includes('name'),
x => x.toLowerCase().includes('title'),
x => x.toLowerCase().includes('subject'),
// x => x.toLowerCase().includes('text'),
// x => x.toLowerCase().includes('desc'),
x => x =>
table.columns table.columns
.find(y => y.columnName == x) .find(y => y.columnName == x)
@@ -44,9 +47,20 @@ function getPerspectiveDefaultColumns(
return [[columns[0]], null]; return [[columns[0]], null];
} }
function getPerspectiveDefaultCollectionColumns(pattern: PerspectiveDataPattern): string[] {
const columns = pattern.columns.map(x => x.name);
const predicates = [...namePredicates, x => pattern.columns.find(y => y.name == x)?.types?.includes('string')];
for (const predicate of predicates) {
const col = columns.find(predicate);
if (col) return [col];
}
}
export function perspectiveNodesHaveStructure( export function perspectiveNodesHaveStructure(
config: PerspectiveConfig, config: PerspectiveConfig,
dbInfos: MultipleDatabaseInfo, dbInfos: MultipleDatabaseInfo,
dataPatterns: PerspectiveDataPatternDict,
conid: string, conid: string,
database: string database: string
) { ) {
@@ -56,8 +70,10 @@ export function perspectiveNodesHaveStructure(
const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const collection = db.collections.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
if (!table && !view) return false; if (!table && !view && !collection) return false;
if (collection && !dataPatterns?.[node.designerId]) return false;
} }
return true; return true;
@@ -66,18 +82,20 @@ export function perspectiveNodesHaveStructure(
export function shouldProcessPerspectiveDefaultColunns( export function shouldProcessPerspectiveDefaultColunns(
config: PerspectiveConfig, config: PerspectiveConfig,
dbInfos: MultipleDatabaseInfo, dbInfos: MultipleDatabaseInfo,
dataPatterns: PerspectiveDataPatternDict,
conid: string, conid: string,
database: string database: string
) { ) {
const nodesNotProcessed = config.nodes.filter(x => !x.defaultColumnsProcessed); const nodesNotProcessed = config.nodes.filter(x => !x.defaultColumnsProcessed);
if (nodesNotProcessed.length == 0) return false; if (nodesNotProcessed.length == 0) return false;
return perspectiveNodesHaveStructure(config, dbInfos, conid, database); return perspectiveNodesHaveStructure(config, dbInfos, dataPatterns, conid, database);
} }
function processPerspectiveDefaultColunnsStep( function processPerspectiveDefaultColunnsStep(
config: PerspectiveConfig, config: PerspectiveConfig,
dbInfos: MultipleDatabaseInfo, dbInfos: MultipleDatabaseInfo,
dataPatterns: PerspectiveDataPatternDict,
conid: string, conid: string,
database: string database: string
) { ) {
@@ -107,6 +125,7 @@ function processPerspectiveDefaultColunnsStep(
const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const collection = db.collections.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
if (table || view) { if (table || view) {
const treeNode = root.findNodeByDesignerId(node.designerId); const treeNode = root.findNodeByDesignerId(node.designerId);
@@ -181,6 +200,22 @@ function processPerspectiveDefaultColunnsStep(
}; };
} }
} }
if (collection) {
const defaultColumns = getPerspectiveDefaultCollectionColumns(dataPatterns?.[node.designerId]);
return {
...config,
nodes: config.nodes.map(n =>
n.designerId == node.designerId
? {
...n,
defaultColumnsProcessed: true,
checkedColumns: defaultColumns,
}
: n
),
};
}
} }
return null; return null;
@@ -199,11 +234,12 @@ function markAllProcessed(config: PerspectiveConfig): PerspectiveConfig {
export function processPerspectiveDefaultColunns( export function processPerspectiveDefaultColunns(
config: PerspectiveConfig, config: PerspectiveConfig,
dbInfos: MultipleDatabaseInfo, dbInfos: MultipleDatabaseInfo,
dataPatterns: PerspectiveDataPatternDict,
conid: string, conid: string,
database: string database: string
): PerspectiveConfig { ): PerspectiveConfig {
while (config.nodes.filter(x => !x.defaultColumnsProcessed).length > 0) { while (config.nodes.filter(x => !x.defaultColumnsProcessed).length > 0) {
const newConfig = processPerspectiveDefaultColunnsStep(config, dbInfos, conid, database); const newConfig = processPerspectiveDefaultColunnsStep(config, dbInfos, dataPatterns, conid, database);
if (!newConfig) { if (!newConfig) {
return markAllProcessed(config); return markAllProcessed(config);
} }

View File

@@ -1,4 +1,3 @@
import { TableInfo } from 'dbgate-types';
import { PerspectiveDisplay } from '../PerspectiveDisplay'; import { PerspectiveDisplay } from '../PerspectiveDisplay';
import { PerspectiveTableNode } from '../PerspectiveTreeNode'; import { PerspectiveTableNode } from '../PerspectiveTreeNode';
import { chinookDbInfo } from './chinookDbInfo'; import { chinookDbInfo } from './chinookDbInfo';
@@ -13,6 +12,7 @@ test('test flat view', () => {
const configColumns = processPerspectiveDefaultColunns( const configColumns = processPerspectiveDefaultColunns(
createPerspectiveConfig({ pureName: 'Artist' }), createPerspectiveConfig({ pureName: 'Artist' }),
{ conid: { db: chinookDbInfo } }, { conid: { db: chinookDbInfo } },
null,
'conid', 'conid',
'db' 'db'
); );
@@ -47,7 +47,7 @@ test('test one level nesting', () => {
columns: [{ source: 'ArtistId', target: 'ArtistId' }], columns: [{ source: 'ArtistId', target: 'ArtistId' }],
}); });
const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, 'conid', 'db'); const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, null, 'conid', 'db');
// const config = createPerspectiveConfig({ pureName: 'Artist' }); // const config = createPerspectiveConfig({ pureName: 'Artist' });
// config.nodes[0].checkedColumns = ['Album']; // config.nodes[0].checkedColumns = ['Album'];
@@ -107,7 +107,7 @@ test('test two level nesting', () => {
designerId: '2', designerId: '2',
columns: [{ source: 'AlbumId', target: 'AlbumId' }], columns: [{ source: 'AlbumId', target: 'AlbumId' }],
}); });
const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, 'conid', 'db'); const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, null, 'conid', 'db');
const root = new PerspectiveTableNode( const root = new PerspectiveTableNode(
artistTable, artistTable,

View File

@@ -0,0 +1,98 @@
import { PerspectiveDisplay } from '../PerspectiveDisplay';
import { PerspectiveTableNode } from '../PerspectiveTreeNode';
import { createPerspectiveConfig, PerspectiveNodeConfig } from '../PerspectiveConfig';
import { processPerspectiveDefaultColunns } from '../processPerspectiveDefaultColunns';
import { DatabaseAnalyser } from 'dbgate-tools';
import { analyseDataPattern } from '../PerspectiveDataPattern';
import { PerspectiveDataProvider } from '../PerspectiveDataProvider';
const accountData = [
{
name: 'jan',
email: 'jan@foo.co',
follows: [{ name: 'lucie' }, { name: 'petr' }],
nested: { email: 'jan@nest.cz' },
},
{
name: 'romeo',
email: 'romeo@foo.co',
follows: [{ name: 'julie' }, { name: 'wiliam' }],
nested: { email: 'romeo@nest.cz' },
},
];
function createDisplay(cfgFunc?: (cfg: PerspectiveNodeConfig) => void) {
const collectionInfo = {
objectTypeField: 'collections',
pureName: 'Account',
};
const dbInfo = {
...DatabaseAnalyser.createEmptyStructure(),
collections: [collectionInfo],
};
const config = createPerspectiveConfig({ pureName: 'Account' });
const dataPatterns = {
[config.rootDesignerId]: analyseDataPattern(
{
conid: 'conid',
database: 'db',
pureName: 'Account',
},
accountData
),
};
const configColumns = processPerspectiveDefaultColunns(
config,
{ conid: { db: dbInfo } },
dataPatterns,
'conid',
'db'
);
if (cfgFunc) {
cfgFunc(configColumns.nodes[0]);
}
const root = new PerspectiveTableNode(
collectionInfo,
{ conid: { db: dbInfo } },
configColumns,
null,
new PerspectiveDataProvider(null, null, dataPatterns),
{ conid: 'conid', database: 'db' },
null,
configColumns.rootDesignerId
);
const display = new PerspectiveDisplay(root, accountData);
return display;
}
test('test nosql display', () => {
const display = createDisplay();
expect(display.rows.length).toEqual(2);
expect(display.rows[0].rowData).toEqual(['jan']);
expect(display.rows[1].rowData).toEqual(['romeo']);
});
test('test nosql nested array display', () => {
const display = createDisplay(cfg => {
cfg.checkedColumns = ['name', 'follows::name'];
});
expect(display.rows.length).toEqual(4);
expect(display.rows[0].rowData).toEqual(['jan', 'lucie']);
expect(display.rows[1].rowData).toEqual([undefined, 'petr']);
expect(display.rows[2].rowData).toEqual(['romeo', 'julie']);
expect(display.rows[3].rowData).toEqual([undefined, 'wiliam']);
});
test('test nosql nested object', () => {
const display = createDisplay(cfg => {
cfg.checkedColumns = ['name', 'nested::email'];
});
expect(display.rows.length).toEqual(2);
expect(display.rows[0].rowData).toEqual(['jan', 'jan@nest.cz']);
expect(display.rows[1].rowData).toEqual(['romeo', 'romeo@nest.cz']);
});

View File

@@ -31,7 +31,7 @@
"typescript": "^4.4.3" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"dbgate-sqltree": "^5.0.0-alpha.1", "dbgate-sqltree": "^5.0.0-alpha.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"json-stable-stringify": "^1.0.1", "json-stable-stringify": "^1.0.1",

View File

@@ -32,6 +32,12 @@ export class SqlDumper implements AlterProcessor {
dialect: SqlDialect; dialect: SqlDialect;
indentLevel = 0; indentLevel = 0;
static keywordsCase = 'upperCase';
static convertKeywordCase(keyword: any): string {
if (this.keywordsCase == 'lowerCase') return keyword?.toString()?.toLowerCase();
return keyword?.toString()?.toUpperCase();
}
constructor(driver: EngineDriver) { constructor(driver: EngineDriver) {
this.driver = driver; this.driver = driver;
this.dialect = driver.dialect; this.dialect = driver.dialect;
@@ -60,10 +66,10 @@ export class SqlDumper implements AlterProcessor {
this.putRaw("'"); this.putRaw("'");
} }
putByteArrayValue(value) { putByteArrayValue(value) {
this.putRaw('NULL'); this.put('^null');
} }
putValue(value) { putValue(value) {
if (value === null) this.putRaw('NULL'); if (value === null) this.put('^null');
else if (value === true) this.putRaw('1'); else if (value === true) this.putRaw('1');
else if (value === false) this.putRaw('0'); else if (value === false) this.putRaw('0');
else if (_isString(value)) this.putStringValue(value); else if (_isString(value)) this.putStringValue(value);
@@ -71,7 +77,7 @@ export class SqlDumper implements AlterProcessor {
else if (_isDate(value)) this.putStringValue(new Date(value).toISOString()); else if (_isDate(value)) this.putStringValue(new Date(value).toISOString());
else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data); else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data);
else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value)); else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value));
else this.putRaw('NULL'); else this.put('^null');
} }
putCmd(format, ...args) { putCmd(format, ...args) {
this.put(format, ...args); this.put(format, ...args);
@@ -92,7 +98,7 @@ export class SqlDumper implements AlterProcessor {
case 'k': case 'k':
{ {
if (value) { if (value) {
this.putRaw(value.toUpperCase()); this.putRaw(SqlDumper.convertKeywordCase(value));
} }
} }
break; break;
@@ -128,7 +134,7 @@ export class SqlDumper implements AlterProcessor {
switch (c) { switch (c) {
case '^': case '^':
while (i < length && format[i].match(/[a-z0-9_]/i)) { while (i < length && format[i].match(/[a-z0-9_]/i)) {
this.putRaw(format[i].toUpperCase()); this.putRaw(SqlDumper.convertKeywordCase(format[i]));
i++; i++;
} }
break; break;
@@ -181,6 +187,14 @@ export class SqlDumper implements AlterProcessor {
this.put(' ^auto_increment'); this.put(' ^auto_increment');
} }
createDatabase(name: string) {
this.putCmd('^create ^database %i', name);
}
dropDatabase(name: string) {
this.putCmd('^drop ^database %i', name);
}
specialColumnOptions(column) {} specialColumnOptions(column) {}
columnDefinition(column: ColumnInfo, { includeDefault = true, includeNullable = true, includeCollate = true } = {}) { columnDefinition(column: ColumnInfo, { includeDefault = true, includeNullable = true, includeCollate = true } = {}) {

View File

@@ -16,6 +16,7 @@ const dialect = {
isSparse: false, isSparse: false,
isPersisted: false, isPersisted: false,
}, },
defaultSchemaName: null,
}; };
export const driverBase = { export const driverBase = {

View File

@@ -1,4 +1,4 @@
import { DatabaseInfo, TableInfo, ApplicationDefinition } from 'dbgate-types'; import { DatabaseInfo, TableInfo, ApplicationDefinition, ViewInfo, CollectionInfo } from 'dbgate-types';
import _flatten from 'lodash/flatten'; import _flatten from 'lodash/flatten';
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo { export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
@@ -118,3 +118,15 @@ export function isTableColumnUnique(table: TableInfo, column: string) {
} }
return false; return false;
} }
export function isTableInfo(obj: { objectTypeField?: string }): obj is TableInfo {
return obj.objectTypeField == 'tables';
}
export function isViewInfo(obj: { objectTypeField?: string }): obj is ViewInfo {
return obj.objectTypeField == 'views';
}
export function isCollectionInfo(obj: { objectTypeField?: string }): obj is CollectionInfo {
return obj.objectTypeField == 'collections';
}

View File

@@ -9,6 +9,7 @@ export interface SqlDialect {
fallbackDataType?: string; fallbackDataType?: string;
explicitDropConstraint?: boolean; explicitDropConstraint?: boolean;
anonymousPrimaryKey?: boolean; anonymousPrimaryKey?: boolean;
defaultSchemaName?: string;
enableConstraintsPerTable?: boolean; enableConstraintsPerTable?: boolean;
dropColumnDependencies?: string[]; dropColumnDependencies?: string[];

View File

@@ -14,6 +14,8 @@ export interface SqlDumper extends AlterProcessor {
putValue(value: string | number | Date); putValue(value: string | number | Date);
putCollection<T>(delimiter: string, collection: T[], lambda: (item: T) => void); putCollection<T>(delimiter: string, collection: T[], lambda: (item: T) => void);
transform(type: TransformType, dumpExpr: () => void); transform(type: TransformType, dumpExpr: () => void);
createDatabase(name: string);
dropDatabase(name: string);
endCommand(); endCommand();
allowIdentityInsert(table: NamedObjectInfo, allow: boolean); allowIdentityInsert(table: NamedObjectInfo, allow: boolean);

View File

@@ -89,9 +89,7 @@ export interface EngineDriver {
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>; ): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
analyseSingleTable(pool: any, name: NamedObjectInfo): Promise<TableInfo>; analyseSingleTable(pool: any, name: NamedObjectInfo): Promise<TableInfo>;
getVersion(pool: any): Promise<{ version: string }>; getVersion(pool: any): Promise<{ version: string }>;
listDatabases( listDatabases(pool: any): Promise<
pool: any
): Promise<
{ {
name: string; name: string;
}[] }[]
@@ -112,7 +110,8 @@ export interface EngineDriver {
updateCollection(pool: any, changeSet: any): Promise<any>; updateCollection(pool: any, changeSet: any): Promise<any>;
getCollectionUpdateScript(changeSet: any): string; getCollectionUpdateScript(changeSet: any): string;
createDatabase(pool: any, name: string): Promise; createDatabase(pool: any, name: string): Promise;
getQuerySplitterOptions(usage: 'stream' | 'script'): any; dropDatabase(pool: any, name: string): Promise;
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor'): any;
script(pool: any, sql: string): Promise; script(pool: any, sql: string): Promise;
getNewObjectTemplates(): NewObjectTemplate[]; getNewObjectTemplates(): NewObjectTemplate[];
// direct call of pool method, only some methods could be supported, on only some drivers // direct call of pool method, only some methods could be supported, on only some drivers

View File

@@ -24,7 +24,7 @@
"chartjs-adapter-moment": "^1.0.0", "chartjs-adapter-moment": "^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dbgate-datalib": "^5.0.0-alpha.1", "dbgate-datalib": "^5.0.0-alpha.1",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"dbgate-sqltree": "^5.0.0-alpha.1", "dbgate-sqltree": "^5.0.0-alpha.1",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"dbgate-types": "^5.0.0-alpha.1", "dbgate-types": "^5.0.0-alpha.1",

View File

@@ -167,3 +167,33 @@ textarea {
color: var(--theme-font-1); color: var(--theme-font-1);
border: 1px solid var(--theme-border); border: 1px solid var(--theme-border);
} }
.ace_gutter-cell.ace-gutter-sql-run {
background-repeat: no-repeat;
background-position: 2px center;
/* content: '▶';
margin-right: 3px; */
/* border-radius: 20px 0px 0px 20px; */
/* Change the color of the breakpoint if you want
box-shadow: 0px 0px 1px 1px #248c46 inset; */
}
.theme-type-light .ace_gutter-cell.ace-gutter-sql-run {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTcuODA0IDE3LjgwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcuODA0IDE3LjgwNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJjOThfcGxheSI+CgkJPHBhdGggZmlsbD0nIzQ0NCcgZD0iTTIuMDY3LDAuMDQzQzIuMjEtMC4wMjgsMi4zNzItMC4wMDgsMi40OTMsMC4wODVsMTMuMzEyLDguNTAzYzAuMDk0LDAuMDc4LDAuMTU0LDAuMTkxLDAuMTU0LDAuMzEzCgkJCWMwLDAuMTItMC4wNjEsMC4yMzctMC4xNTQsMC4zMTRMMi40OTIsMTcuNzE3Yy0wLjA3LDAuMDU3LTAuMTYyLDAuMDg3LTAuMjUsMC4wODdsLTAuMTc2LTAuMDQKCQkJYy0wLjEzNi0wLjA2NS0wLjIyMi0wLjIwNy0wLjIyMi0wLjM2MVYwLjQwMkMxLjg0NCwwLjI1LDEuOTMsMC4xMDcsMi4wNjcsMC4wNDN6Ii8+Cgk8L2c+Cgk8ZyBpZD0iQ2FwYV8xXzc4XyI+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==);
}
.theme-type-dark .ace_gutter-cell.ace-gutter-sql-run {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTcuODA0IDE3LjgwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcuODA0IDE3LjgwNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJjOThfcGxheSI+CgkJPHBhdGggZmlsbD0nI2RkZCcgZD0iTTIuMDY3LDAuMDQzQzIuMjEtMC4wMjgsMi4zNzItMC4wMDgsMi40OTMsMC4wODVsMTMuMzEyLDguNTAzYzAuMDk0LDAuMDc4LDAuMTU0LDAuMTkxLDAuMTU0LDAuMzEzCgkJCWMwLDAuMTItMC4wNjEsMC4yMzctMC4xNTQsMC4zMTRMMi40OTIsMTcuNzE3Yy0wLjA3LDAuMDU3LTAuMTYyLDAuMDg3LTAuMjUsMC4wODdsLTAuMTc2LTAuMDQKCQkJYy0wLjEzNi0wLjA2NS0wLjIyMi0wLjIwNy0wLjIyMi0wLjM2MVYwLjQwMkMxLjg0NCwwLjI1LDEuOTMsMC4xMDcsMi4wNjcsMC4wNDN6Ii8+Cgk8L2c+Cgk8ZyBpZD0iQ2FwYV8xXzc4XyI+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==);
}
.ace_gutter-cell.ace-gutter-sql-run:hover {
background-color: var(--theme-bg-2);
}
.ace_gutter-cell.ace-gutter-current-part {
/* background-color: var(--theme-bg-2); */
font-weight: bold;
color: var(--theme-font-hover);
}

View File

@@ -19,6 +19,7 @@
import AppTitleProvider from './utility/AppTitleProvider.svelte'; import AppTitleProvider from './utility/AppTitleProvider.svelte';
import getElectron from './utility/getElectron'; import getElectron from './utility/getElectron';
import AppStartInfo from './widgets/AppStartInfo.svelte'; import AppStartInfo from './widgets/AppStartInfo.svelte';
import SettingsListener from './utility/SettingsListener.svelte';
let loadedApi = false; let loadedApi = false;
let loadedPlugins = false; let loadedPlugins = false;
@@ -79,6 +80,7 @@
<AppTitleProvider /> <AppTitleProvider />
{#if loadedPlugins} {#if loadedPlugins}
<OpenTabsOnStartup /> <OpenTabsOnStartup />
<SettingsListener />
<Screen /> <Screen />
{:else} {:else}
<AppStartInfo <AppStartInfo

View File

@@ -77,6 +77,17 @@
); );
}; };
const handleDropDatabase = () => {
showModal(ConfirmModal, {
message: `Really drop database ${name}? All opened sessions with this database will be forcefully closed.`,
onConfirm: () =>
apiCall('server-connections/drop-database', {
conid: connection._id,
name,
}),
});
};
const handleNewCollection = () => { const handleNewCollection = () => {
showModal(InputTextModal, { showModal(InputTextModal, {
value: '', value: '',
@@ -218,6 +229,30 @@
}); });
}; };
const handleQueryDesigner = () => {
openNewTab({
title: 'Query #',
icon: 'img query-design',
tabComponent: 'QueryDesignTab',
props: {
conid: connection._id,
database: name,
},
});
};
const handleNewPerspective = () => {
openNewTab({
title: 'Perspective #',
icon: 'img perspective',
tabComponent: 'PerspectiveTab',
props: {
conid: connection._id,
database: name,
},
});
};
async function handleConfirmSql(sql) { async function handleConfirmSql(sql) {
saveScriptToDatabase({ conid: connection._id, database: name }, sql, false); saveScriptToDatabase({ conid: connection._id, database: name }, sql, false);
} }
@@ -233,13 +268,21 @@
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true }, { onClick: handleNewQuery, text: 'New query', isNewQuery: true },
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: 'New table' }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: 'New table' },
driver?.databaseEngineTypes?.includes('document') && { onClick: handleNewCollection, text: 'New collection' }, driver?.databaseEngineTypes?.includes('document') && { onClick: handleNewCollection, text: 'New collection' },
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleQueryDesigner, text: 'Design query' },
driver?.databaseEngineTypes?.includes('sql') && {
onClick: handleNewPerspective,
text: 'Design perspective query',
},
{ divider: true }, { divider: true },
isSqlOrDoc && !connection.isReadOnly && { onClick: handleImport, text: 'Import wizard' }, isSqlOrDoc && !connection.isReadOnly && { onClick: handleImport, text: 'Import wizard' },
isSqlOrDoc && { onClick: handleExport, text: 'Export wizard' }, isSqlOrDoc && { onClick: handleExport, text: 'Export wizard' },
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlRestore, text: 'Restore/import SQL dump' }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlRestore, text: 'Restore/import SQL dump' },
driver?.supportsDatabaseDump && { onClick: handleSqlDump, text: 'Backup/export SQL dump' }, driver?.supportsDatabaseDump && { onClick: handleSqlDump, text: 'Backup/export SQL dump' },
isSqlOrDoc &&
!connection.isReadOnly &&
!connection.singleDatabase && { onClick: handleDropDatabase, text: 'Drop database' },
{ divider: true }, { divider: true },
isSqlOrDoc && { onClick: handleShowDiagram, text: 'Show diagram' }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: 'Show diagram' },
isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' }, isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' },
isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' }, isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' },
isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' }, isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' },

View File

@@ -9,7 +9,7 @@
schemaName, schemaName,
...(columns?.map(({ columnName }) => ({ childName: columnName })) || []) ...(columns?.map(({ columnName }) => ({ childName: columnName })) || [])
); );
export const createTitle = ({ pureName }) => pureName; export const createTitle = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
export const databaseObjectIcons = { export const databaseObjectIcons = {
tables: 'img table', tables: 'img table',
@@ -52,7 +52,12 @@
icon: 'img table-structure', icon: 'img table-structure',
}, },
{ {
label: 'Open perspective', label: 'Design query',
isQueryDesigner: true,
requiresWriteAccess: true,
},
{
label: 'Design perspective query',
tab: 'PerspectiveTab', tab: 'PerspectiveTab',
forceNewTab: true, forceNewTab: true,
icon: 'img perspective', icon: 'img perspective',
@@ -80,11 +85,6 @@
isDuplicateTable: true, isDuplicateTable: true,
requiresWriteAccess: true, requiresWriteAccess: true,
}, },
{
label: 'Query designer',
isQueryDesigner: true,
requiresWriteAccess: true,
},
{ {
label: 'Show diagram', label: 'Show diagram',
isDiagram: true, isDiagram: true,
@@ -155,7 +155,11 @@
icon: 'img view-structure', icon: 'img view-structure',
}, },
{ {
label: 'Open perspective', label: 'Design query',
isQueryDesigner: true,
},
{
label: 'Design perspective query',
tab: 'PerspectiveTab', tab: 'PerspectiveTab',
forceNewTab: true, forceNewTab: true,
icon: 'img perspective', icon: 'img perspective',
@@ -164,10 +168,6 @@
label: 'Drop view', label: 'Drop view',
isDrop: true, isDrop: true,
}, },
{
label: 'Query designer',
isQueryDesigner: true,
},
{ {
divider: true, divider: true,
}, },
@@ -345,6 +345,12 @@
}, },
}, },
}, },
{
label: 'Design perspective query',
tab: 'PerspectiveTab',
forceNewTab: true,
icon: 'img perspective',
},
{ {
label: 'Export', label: 'Export',
isExport: true, isExport: true,
@@ -586,6 +592,16 @@
} }
} }
function getObjectTitle(connection, schemaName, pureName) {
const driver = findEngineDriver(connection, getExtensions());
const defaultSchema = driver?.dialect?.defaultSchemaName;
if (schemaName && defaultSchema && schemaName != defaultSchema) {
return `${schemaName}.${pureName}`;
}
return pureName;
}
export async function openDatabaseObjectDetail( export async function openDatabaseObjectDetail(
tabComponent, tabComponent,
scriptTemplate, scriptTemplate,
@@ -603,7 +619,7 @@
openNewTab( openNewTab(
{ {
title: scriptTemplate ? 'Query #' : pureName, title: scriptTemplate ? 'Query #' : getObjectTitle(connection, schemaName, pureName),
tooltip, tooltip,
icon: icon || (scriptTemplate ? 'img sql-file' : databaseObjectIcons[objectTypeField]), icon: icon || (scriptTemplate ? 'img sql-file' : databaseObjectIcons[objectTypeField]),
tabComponent: scriptTemplate ? 'QueryTab' : tabComponent, tabComponent: scriptTemplate ? 'QueryTab' : tabComponent,

View File

@@ -127,6 +127,9 @@ registerCommand({
name: 'Query design', name: 'Query design',
menuName: 'New query design', menuName: 'New query design',
onClick: () => newQueryDesign(), onClick: () => newQueryDesign(),
testEnabled: () =>
getCurrentDatabase() &&
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
}); });
registerCommand({ registerCommand({
@@ -144,6 +147,9 @@ registerCommand({
icon: 'img diagram', icon: 'img diagram',
name: 'ER Diagram', name: 'ER Diagram',
menuName: 'New ER diagram', menuName: 'New ER diagram',
testEnabled: () =>
getCurrentDatabase() &&
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
onClick: () => newDiagram(), onClick: () => newDiagram(),
}); });
@@ -599,7 +605,7 @@ export function registerFileCommands({
registerCommand({ registerCommand({
id: idPrefix + '.replace', id: idPrefix + '.replace',
category, category,
keyText: 'CtrlOrCommand+H', keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H',
name: 'Replace', name: 'Replace',
testEnabled: () => getCurrentEditor() != null, testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().replace(), onClick: () => getCurrentEditor().replace(),

View File

@@ -200,7 +200,7 @@
id: 'dataGrid.hideColumn', id: 'dataGrid.hideColumn',
category: 'Data grid', category: 'Data grid',
name: 'Hide column', name: 'Hide column',
keyText: 'CtrlOrCommand+H', keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H',
testEnabled: () => getCurrentDataGrid() != null, testEnabled: () => getCurrentDataGrid() != null,
onClick: () => getCurrentDataGrid().hideColumn(), onClick: () => getCurrentDataGrid().hideColumn(),
}); });

View File

@@ -65,7 +65,7 @@
let changeIndex = 0; let changeIndex = 0;
let rowCountLoaded = null; let rowCountLoaded = null;
const throttleLoadNext = _.throttle(() => domGrid.resetLoadedAll(), 500); const throttleLoadNext = _.throttle(() => domGrid?.resetLoadedAll(), 500);
const handleJslDataStats = stats => { const handleJslDataStats = stats => {
if (stats.changeIndex < changeIndex) return; if (stats.changeIndex < changeIndex) return;

View File

@@ -61,6 +61,9 @@
} }
$: sortOrderProps = settings?.getSortOrderProps ? settings?.getSortOrderProps(designerId, column.columnName) : null; $: sortOrderProps = settings?.getSortOrderProps ? settings?.getSortOrderProps(designerId, column.columnName) : null;
$: iconOverride = settings?.getColumnIconOverride
? settings?.getColumnIconOverride(designerId, column.columnName)
: null;
</script> </script>
<div <div
@@ -144,7 +147,7 @@
}} }}
/> />
{/if} {/if}
<ColumnLabel {...column} {foreignKey} forceIcon /> <ColumnLabel {...column} {foreignKey} forceIcon {iconOverride} />
{#if designerColumn?.filter} {#if designerColumn?.filter}
<FontIcon icon="img filter" /> <FontIcon icon="img filter" />
{/if} {/if}

View File

@@ -479,7 +479,7 @@
const rect = e.target.getBoundingClientRect(); const rect = e.target.getBoundingClientRect();
var json = JSON.parse(data); var json = JSON.parse(data);
const { objectTypeField } = json; const { objectTypeField } = json;
if (objectTypeField != 'tables' && objectTypeField != 'views') return; if (objectTypeField != 'tables' && objectTypeField != 'views' && objectTypeField != 'collections') return;
json.designerId = `${json.pureName}-${uuidv1()}`; json.designerId = `${json.pureName}-${uuidv1()}`;
json.left = e.clientX - rect.left; json.left = e.clientX - rect.left;
json.top = e.clientY - rect.top; json.top = e.clientY - rect.top;
@@ -941,6 +941,7 @@
.empty { .empty {
margin: 50px; margin: 50px;
font-size: 20px; font-size: 20px;
position: absolute;
} }
.canvas { .canvas {
position: relative; position: relative;

View File

@@ -213,6 +213,8 @@
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })], !isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
]; ];
} }
// $: console.log('COLUMNS', columns);
</script> </script>
<div <div
@@ -238,6 +240,7 @@
class:isGrayed class:isGrayed
class:isTable={objectTypeField == 'tables'} class:isTable={objectTypeField == 'tables'}
class:isView={objectTypeField == 'views'} class:isView={objectTypeField == 'views'}
class:isCollection={objectTypeField == 'collections'}
use:moveDrag={settings?.canSelectColumns ? [handleMoveStart, handleMove, handleMoveEnd] : null} use:moveDrag={settings?.canSelectColumns ? [handleMoveStart, handleMove, handleMoveEnd] : null}
use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'} use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
style={getTableColorStyle($currentThemeDefinition, table)} style={getTableColorStyle($currentThemeDefinition, table)}
@@ -358,6 +361,10 @@
.header.isView { .header.isView {
background: var(--theme-bg-magenta); background: var(--theme-bg-magenta);
} }
.header.isCollection {
background: var(--theme-bg-red);
}
.header.isGrayed { .header.isGrayed {
background: var(--theme-bg-2); background: var(--theme-bg-2);
} }

View File

@@ -23,8 +23,9 @@
export let foreignKey; export let foreignKey;
export let conid = undefined; export let conid = undefined;
export let database = undefined; export let database = undefined;
export let iconOverride = undefined;
$: icon = getColumnIcon($$props, forceIcon); $: icon = iconOverride || getColumnIcon($$props, forceIcon);
</script> </script>
<span class="label" class:notNull> <span class="label" class:notNull>

View File

@@ -22,7 +22,7 @@
<div class="wrapper"> <div class="wrapper">
<InlineButton <InlineButton
on:click={() => { on:click={() => {
skip -= limit; skip = parseInt(skip) - parseInt(limit);
if (skip < 0) skip = 0; if (skip < 0) skip = 0;
dispatch('load'); dispatch('load');
}} }}
@@ -35,7 +35,7 @@
<TextField type="number" bind:value={limit} on:blur={() => dispatch('load')} on:keydown={handleKeyDown} /> <TextField type="number" bind:value={limit} on:blur={() => dispatch('load')} on:keydown={handleKeyDown} />
<InlineButton <InlineButton
on:click={() => { on:click={() => {
skip += limit; skip = parseInt(skip) + parseInt(limit);
dispatch('load'); dispatch('load');
}} }}
> >

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
export let unsaved = false;
let mousein = false;
</script>
<span
class="close-button tabCloseButton"
on:click
on:mouseenter={() => {
mousein = true;
}}
on:mouseleave={() => {
mousein = false;
}}
>
<FontIcon icon={unsaved && !mousein ? 'icon unsaved' : 'icon close'} />
</span>
<style>
.close-button {
margin-left: 5px;
color: var(--theme-font-3);
}
.close-button:hover {
color: var(--theme-font-1);
}
</style>

View File

@@ -46,6 +46,7 @@
'icon file': 'mdi mdi-file', 'icon file': 'mdi mdi-file',
'icon loading': 'mdi mdi-loading mdi-spin', 'icon loading': 'mdi mdi-loading mdi-spin',
'icon close': 'mdi mdi-close', 'icon close': 'mdi mdi-close',
'icon unsaved': 'mdi mdi-record',
'icon stop': 'mdi mdi-close-octagon', 'icon stop': 'mdi mdi-close-octagon',
'icon filter': 'mdi mdi-filter', 'icon filter': 'mdi mdi-filter',
'icon filter-off': 'mdi mdi-filter-off', 'icon filter-off': 'mdi mdi-filter-off',

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
export let tabs;
export let onConfirm;
export let onCancel;
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Confirm close tabs</svelte:fragment>
<div>
Following files are modified, really close tabs? After closing, you could reopen them in history
<FontIcon icon="icon history" />
widget
</div>
{#each tabs as tab}
<div class="ml-2"><FontIcon icon={tab.icon} /> {tab.title}</div>
{/each}
<svelte:fragment slot="footer">
<FormSubmit
value="Close tabs"
on:click={() => {
closeCurrentModal();
onConfirm();
}}
/>
<FormStyledButton
type="button"
value="Cancel"
on:click={() => {
closeCurrentModal();
onCancel();
}}
/>
</svelte:fragment>
</ModalBase>
</FormProvider>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { SqlDumper } from 'dbgate-tools';
import FormStyledButton from '../buttons/FormStyledButton.svelte'; import FormStyledButton from '../buttons/FormStyledButton.svelte';
import TableControl from '../elements/TableControl.svelte'; import TableControl from '../elements/TableControl.svelte';
import TextField from '../forms/TextField.svelte'; import TextField from '../forms/TextField.svelte';
@@ -63,9 +64,11 @@
const source = sources[sourceIndex]; const source = sources[sourceIndex];
const target = targets[targetIndex]; const target = targets[targetIndex];
if (source && target) { if (source && target) {
return `${JOIN_TYPES[joinIndex]} ${target.refTable}${alias ? ` ${alias}` : ''} ON ${target.columnMap return `${SqlDumper.convertKeywordCase(JOIN_TYPES[joinIndex])} ${target.refTable}${
alias ? ` ${alias}` : ''
} ${SqlDumper.convertKeywordCase('ON')} ${target.columnMap
.map(col => `${source.name}.${col.columnName} = ${alias || target.refTable}.${col.refColumnName}`) .map(col => `${source.name}.${col.columnName} = ${alias || target.refTable}.${col.refColumnName}`)
.join(' AND ')}`; .join(SqlDumper.convertKeywordCase(' AND '))}`;
} }
return ''; return '';
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getAsImageSrc, safeJsonParse } from 'dbgate-tools'; import { getAsImageSrc, safeJsonParse } from 'dbgate-tools';
import { isArray } from 'lodash'; import _ from 'lodash';
import CellValue from '../datagrid/CellValue.svelte'; import CellValue from '../datagrid/CellValue.svelte';
import JSONTree from '../jsontree/JSONTree.svelte'; import JSONTree from '../jsontree/JSONTree.svelte';
@@ -12,7 +12,7 @@
export let displayType; export let displayType;
</script> </script>
<td rowspan={rowSpan} data-column={columnIndex} class:isEmpty={value===undefined}> <td rowspan={rowSpan} data-column={columnIndex} class:isEmpty={value === undefined}>
{#if value !== undefined} {#if value !== undefined}
{#if displayType == 'json'} {#if displayType == 'json'}
<JSONTree value={safeJsonParse(value, value?.toString())} slicedKeyCount={1} disableContextMenu /> <JSONTree value={safeJsonParse(value, value?.toString())} slicedKeyCount={1} disableContextMenu />
@@ -23,6 +23,8 @@
{:else} {:else}
<span class="null"> (no image)</span> <span class="null"> (no image)</span>
{/if} {/if}
{:else if !value.$oid && (_.isArray(value) || _.isPlainObject(value))}
<JSONTree {value} slicedKeyCount={1} disableContextMenu />
{:else} {:else}
<CellValue {rowData} {value} /> <CellValue {rowData} {value} />
{/if} {/if}

View File

@@ -3,10 +3,12 @@
createPerspectiveNodeConfig, createPerspectiveNodeConfig,
MultipleDatabaseInfo, MultipleDatabaseInfo,
PerspectiveConfig, PerspectiveConfig,
PerspectiveDataPatternDict,
perspectiveNodesHaveStructure, perspectiveNodesHaveStructure,
PerspectiveTreeNode, PerspectiveTreeNode,
switchPerspectiveReferenceDirection, switchPerspectiveReferenceDirection,
} from 'dbgate-datalib'; } from 'dbgate-datalib';
import { CollectionInfo } from 'dbgate-types';
import _ from 'lodash'; import _ from 'lodash';
import { tick } from 'svelte'; import { tick } from 'svelte';
import runCommand from '../commands/runCommand'; import runCommand from '../commands/runCommand';
@@ -18,6 +20,7 @@
export let config: PerspectiveConfig; export let config: PerspectiveConfig;
export let dbInfos: MultipleDatabaseInfo; export let dbInfos: MultipleDatabaseInfo;
export let dataPatterns: PerspectiveDataPatternDict;
export let root: PerspectiveTreeNode; export let root: PerspectiveTreeNode;
export let conid; export let conid;
@@ -27,22 +30,39 @@
export let onClickTableHeader = null; export let onClickTableHeader = null;
function createDesignerModel(config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo) { function createDesignerModel(
config: PerspectiveConfig,
dbInfos: MultipleDatabaseInfo,
dataPatterns: PerspectiveDataPatternDict
) {
return { return {
...config, ...config,
tables: _.compact( tables: _.compact(
config.nodes.map(node => { config.nodes.map(node => {
const table = dbInfos?.[node.conid || conid]?.[node.database || database]?.tables?.find( const db = dbInfos?.[node.conid || conid]?.[node.database || database];
const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName);
let collection: CollectionInfo & { columns?: any[] } = db?.collections?.find(
x => x.pureName == node.pureName && x.schemaName == node.schemaName x => x.pureName == node.pureName && x.schemaName == node.schemaName
); );
const view = dbInfos?.[node.conid || conid]?.[node.database || database]?.views?.find(
x => x.pureName == node.pureName && x.schemaName == node.schemaName if (collection) {
); const pattern = dataPatterns?.[node.designerId];
if (!table && !view) return null; if (!pattern) return null;
collection = {
...collection,
columns:
pattern?.columns.map(x => ({
columnName: x.name,
})) || [],
};
}
if (!table && !view && !collection) return null;
const { designerId } = node; const { designerId } = node;
return { return {
...(table || view), ...(table || view || collection),
left: node?.position?.x || 0, left: node?.position?.x || 0,
top: node?.position?.y || 0, top: node?.position?.y || 0,
alias: node.alias, alias: node.alias,
@@ -55,7 +75,7 @@
function handleChange(value, skipUndoChain, settings) { function handleChange(value, skipUndoChain, settings) {
setConfig(oldValue => { setConfig(oldValue => {
const newValue = _.isFunction(value) ? value(createDesignerModel(oldValue, dbInfos)) : value; const newValue = _.isFunction(value) ? value(createDesignerModel(oldValue, dbInfos, dataPatterns)) : value;
let isArranged = oldValue.isArranged; let isArranged = oldValue.isArranged;
if (settings?.isCalledFromArrange) { if (settings?.isCalledFromArrange) {
isArranged = true; isArranged = true;
@@ -122,11 +142,11 @@
}); });
} }
async function detectAutoArrange(config: PerspectiveConfig, dbInfos, root) { async function detectAutoArrange(config: PerspectiveConfig, dbInfos, dataPatterns, root) {
if ( if (
root && root &&
config.nodes.find(x => !x.position) && config.nodes.find(x => !x.position) &&
perspectiveNodesHaveStructure(config, dbInfos, conid, database) && perspectiveNodesHaveStructure(config, dbInfos, dataPatterns, conid, database) &&
config.nodes.every(x => root?.findNodeByDesignerId(x.designerId)) config.nodes.every(x => root?.findNodeByDesignerId(x.designerId))
) { ) {
await tick(); await tick();
@@ -134,7 +154,7 @@
} }
} }
$: detectAutoArrange(config, dbInfos, root); $: detectAutoArrange(config, dbInfos, dataPatterns, root);
// $: console.log('DESIGNER ROOT', root); // $: console.log('DESIGNER ROOT', root);
</script> </script>
@@ -221,6 +241,14 @@
const orderIndex = sort.length > 1 ? _.findIndex(sort, x => x.columnName == columnName) : -1; const orderIndex = sort.length > 1 ? _.findIndex(sort, x => x.columnName == columnName) : -1;
return { order, orderIndex }; return { order, orderIndex };
}, },
getColumnIconOverride: (designerId, columnName) => {
const pattern = dataPatterns?.[designerId];
const column = pattern?.columns.find(x => x.name == columnName);
if (column?.types?.includes('json')) {
return 'img json';
}
return null;
},
isColumnFiltered: (designerId, columnName) => { isColumnFiltered: (designerId, columnName) => {
return !!config.nodes.find(x => x.designerId == designerId)?.filters?.[columnName]; return !!config.nodes.find(x => x.designerId == designerId)?.filters?.[columnName];
}, },
@@ -277,6 +305,6 @@
onClickTableHeader, onClickTableHeader,
}} }}
referenceComponent={QueryDesignerReference} referenceComponent={QueryDesignerReference}
value={createDesignerModel(config, dbInfos)} value={createDesignerModel(config, dbInfos, dataPatterns)}
onChange={handleChange} onChange={handleChange}
/> />

View File

@@ -16,6 +16,7 @@
ChangePerspectiveConfigFunc, ChangePerspectiveConfigFunc,
PerspectiveConfig, PerspectiveConfig,
PerspectiveDisplay, PerspectiveDisplay,
PerspectivePatternColumnNode,
PerspectiveTableColumnNode, PerspectiveTableColumnNode,
PerspectiveTreeNode, PerspectiveTreeNode,
PERSPECTIVE_PAGE_SIZE, PERSPECTIVE_PAGE_SIZE,
@@ -41,6 +42,24 @@
import { getFilterValueExpression } from 'dbgate-filterparser'; import { getFilterValueExpression } from 'dbgate-filterparser';
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte'; import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
const TABS_BY_FIELD = {
tables: {
text: 'table',
tabComponent: 'TableDataTab',
icon: 'img table',
},
views: {
text: 'view',
tabComponent: 'ViewDataTab',
icon: 'img view',
},
collections: {
text: 'collection',
tabComponent: 'CollectionDataTab',
icon: 'img collection',
},
};
const dbg = debug('dbgate:PerspectiveTable'); const dbg = debug('dbgate:PerspectiveTable');
export const activator = createActivator('PerspectiveTable', true, ['Designer']); export const activator = createActivator('PerspectiveTable', true, ['Designer']);
@@ -57,6 +76,7 @@
let errorMessage; let errorMessage;
let rowCount; let rowCount;
let isLoading = false; let isLoading = false;
let isLoadQueued = false;
const lastVisibleRowIndexRef = createRef(0); const lastVisibleRowIndexRef = createRef(0);
const disableLoadNextRef = createRef(false); const disableLoadNextRef = createRef(false);
@@ -121,6 +141,12 @@
} }
async function loadData(node: PerspectiveTreeNode, counts) { async function loadData(node: PerspectiveTreeNode, counts) {
if (isLoading) {
isLoadQueued = true;
return;
} else {
isLoadQueued = false;
}
// console.log('LOADING', node); // console.log('LOADING', node);
if (!node) return; if (!node) return;
const rows = []; const rows = [];
@@ -147,6 +173,10 @@
// loadProps.push(child.getNodeLoadProps()); // loadProps.push(child.getNodeLoadProps());
// } // }
// } // }
if (isLoadQueued) {
loadData(root, $loadedCounts);
}
} }
export function openJson() { export function openJson() {
@@ -199,25 +229,29 @@
const tableNode = root?.findNodeByDesignerId(tableNodeDesignerId); const tableNode = root?.findNodeByDesignerId(tableNodeDesignerId);
if (tableNode?.headerTableAttributes) { if (tableNode?.headerTableAttributes) {
const { pureName, schemaName, conid, database } = tableNode?.headerTableAttributes; const { pureName, schemaName, conid, database, objectTypeField } = tableNode?.headerTableAttributes;
console.log('objectTypeField', objectTypeField);
const tab = TABS_BY_FIELD[objectTypeField];
if (tab) {
res.push({ res.push({
text: `Open table ${pureName}`, text: `Open ${tab.text} ${pureName}`,
onClick: () => { onClick: () => {
openNewTab({ openNewTab({
title: pureName, title: pureName,
icon: 'img table', icon: tab.icon,
tabComponent: 'TableDataTab', tabComponent: tab.tabComponent,
props: { props: {
schemaName, schemaName,
pureName, pureName,
conid: conid, conid: conid,
database: database, database: database,
objectTypeField: 'tables', objectTypeField,
}, },
}); });
}, },
}); });
} }
}
const setColumnDisplay = type => { const setColumnDisplay = type => {
setConfig(cfg => ({ setConfig(cfg => ({
@@ -280,42 +314,39 @@
const value = display.rows[rowIndex].rowData[columnIndex]; const value = display.rows[rowIndex].rowData[columnIndex];
const { dataNode } = column; const { dataNode } = column;
if (dataNode instanceof PerspectiveTableColumnNode) { if (
dataNode.filterInfo &&
(dataNode instanceof PerspectiveTableColumnNode || dataNode instanceof PerspectivePatternColumnNode)
) {
const { table } = dataNode; const { table } = dataNode;
let tabComponent = null;
let icon = null; const tab = TABS_BY_FIELD[table.objectTypeField];
let objectTypeField = null; const filterExpression = getFilterValueExpression(
if (dataNode.isTable) { value,
tabComponent = 'TableDataTab'; dataNode instanceof PerspectiveTableColumnNode ? dataNode.column.dataType : null
icon = 'img table'; );
objectTypeField = 'tables';
} if (tab) {
if (dataNode.isView) {
tabComponent = 'ViewDataTab';
icon = 'img view';
objectTypeField = 'views';
}
if (tabComponent) {
res.push({ res.push({
text: 'Open filtered table', text: 'Open filtered grid',
onClick: () => { onClick: () => {
openNewTab( openNewTab(
{ {
title: table.pureName, title: table.pureName,
icon, icon: tab.icon,
tabComponent, tabComponent: tab.tabComponent,
props: { props: {
schemaName: table.schemaName, schemaName: table.schemaName,
pureName: table.pureName, pureName: table.pureName,
conid, conid,
database, database,
objectTypeField, objectTypeField: table.objectTypeField,
}, },
}, },
{ {
grid: { grid: {
filters: { filters: {
[dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType), [dataNode.columnName]: filterExpression,
}, },
// isFormView: true, // isFormView: true,
}, },
@@ -339,7 +370,7 @@
...n, ...n,
filters: { filters: {
...n.filters, ...n.filters,
[dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType), [dataNode.columnName]: filterExpression,
}, },
} }
: n : n

View File

@@ -65,6 +65,7 @@
import { sleep } from '../utility/common'; import { sleep } from '../utility/common';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
import InlineButton from '../buttons/InlineButton.svelte'; import InlineButton from '../buttons/InlineButton.svelte';
import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns';
const dbg = debug('dbgate:PerspectiveView'); const dbg = debug('dbgate:PerspectiveView');
@@ -128,17 +129,21 @@
} }
$: dbInfos = useMultipleDatabaseInfo(perspectiveDatabases); $: dbInfos = useMultipleDatabaseInfo(perspectiveDatabases);
$: loader = new PerspectiveDataLoader(apiCall);
$: dataPatterns = usePerspectiveDataPatterns({ conid, database }, config, cache, $dbInfos, loader);
$: rootObject = config?.nodes?.find(x => x.designerId == config?.rootDesignerId); $: rootObject = config?.nodes?.find(x => x.designerId == config?.rootDesignerId);
$: rootDb = rootObject ? $dbInfos?.[rootObject.conid || conid]?.[rootObject.database || database] : null; $: rootDb = rootObject ? $dbInfos?.[rootObject.conid || conid]?.[rootObject.database || database] : null;
$: tableInfo = rootDb?.tables.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName); $: tableInfo = rootDb?.tables.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: viewInfo = rootDb?.views.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName); $: viewInfo = rootDb?.views.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: collectionInfo = rootDb?.collections.find(
x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName
);
$: loader = new PerspectiveDataLoader(apiCall); $: dataProvider = new PerspectiveDataProvider(cache, loader, $dataPatterns);
$: dataProvider = new PerspectiveDataProvider(cache, loader);
$: root = $: root =
tableInfo || viewInfo tableInfo || viewInfo || collectionInfo
? new PerspectiveTableNode( ? new PerspectiveTableNode(
tableInfo || viewInfo, tableInfo || viewInfo || collectionInfo,
$dbInfos, $dbInfos,
config, config,
setConfig, setConfig,
@@ -151,13 +156,14 @@
$: tempRoot = root?.findNodeByDesignerId(tempRootDesignerId); $: tempRoot = root?.findNodeByDesignerId(tempRootDesignerId);
$: { $: {
if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, conid, database)) { if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, $dataPatterns, conid, database)) {
setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, conid, database)); setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, $dataPatterns, conid, database));
} }
} }
// $: console.log('PERSPECTIVE', config); // $: console.log('PERSPECTIVE', config);
// $: console.log('VIEW ROOT', root); // $: console.log('VIEW ROOT', root);
// $: console.log('dataPatterns', $dataPatterns);
</script> </script>
<HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize} allowCollapseChild1> <HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize} allowCollapseChild1>
@@ -205,6 +211,7 @@
{database} {database}
{setConfig} {setConfig}
dbInfos={$dbInfos} dbInfos={$dbInfos}
dataPatterns={$dataPatterns}
{root} {root}
onClickTableHeader={designerId => { onClickTableHeader={designerId => {
sleep(100).then(() => { sleep(100).then(() => {

View File

@@ -155,6 +155,8 @@
export let readOnly = false; export let readOnly = false;
export let splitterOptions = null; export let splitterOptions = null;
export let onKeyDown = null; export let onKeyDown = null;
export let onExecuteFragment = null;
export let errorMessages = null;
const tabVisible: any = getContext('tabVisible'); const tabVisible: any = getContext('tabVisible');
@@ -166,7 +168,8 @@
let queryParts = []; let queryParts = [];
let currentPart = null; let currentPart = null;
let currentPartMarker = null; let currentPartLines = [];
// let currentPartMarker = null;
let queryParserWorker; let queryParserWorker;
@@ -183,16 +186,26 @@
return editor; return editor;
} }
export function getCurrentCommandText(): string { export function getCurrentCommandText(): { text: string; line?: number } {
if (currentPart != null) return currentPart.text; if (currentPart != null) {
if (!editor) return ''; return {
const selectedText = editor.getSelectedText(); text: currentPart.text,
if (selectedText) return selectedText; line: currentPart.trimStart.line,
if (editor.getHighlightActiveLine()) { };
const line = editor.getSelectionRange().start.row;
return editor.session.getLine(line);
} }
return ''; if (!editor) return { text: '' };
const selectedText = editor.getSelectedText();
if (selectedText) {
return {
text: selectedText,
line: editor.getSelectionRange().start.row,
};
}
const line = editor.getSelectionRange().start.row;
return {
text: editor.session.getLine(line),
line,
};
} }
export function getCodeCompletionCommandText() { export function getCodeCompletionCommandText() {
@@ -285,8 +298,37 @@
function processParserResult(data) { function processParserResult(data) {
queryParts = data; queryParts = data;
editor.setHighlightActiveLine(queryParts.length <= 1); // editor.setHighlightActiveLine(queryParts.length <= 1);
changedCurrentQueryPart(); changedCurrentQueryPart();
updateAnnotations();
}
function updateAnnotations() {
if (!mode?.includes('sql')) return;
// console.log('UPDATING ANNOTATIONS');
editor?.session?.setAnnotations([
...(queryParts || [])
.filter(part => !(errorMessages || []).find(err => err.line == part.trimStart.line))
.map(part => ({
row: part.trimStart.line,
text: part.text,
className: currentPartLines.includes(part.trimStart.line)
? 'ace-gutter-sql-run ace-gutter-current-part'
: 'ace-gutter-sql-run', // className: 'ace-gutter-sql-run',
})),
...(errorMessages || []).map(error => ({
row: error.line,
text: error.message,
type: 'error',
})),
]);
}
$: {
errorMessages;
updateAnnotations();
} }
const handleContextMenu = e => { const handleContextMenu = e => {
@@ -304,8 +346,6 @@
function changedQueryParts() { function changedQueryParts() {
const editor = getEditor(); const editor = getEditor();
if (splitterOptions && editor && queryParserWorker) { if (splitterOptions && editor && queryParserWorker) {
const editor = getEditor();
const message = { const message = {
text: editor.getValue(), text: editor.getValue(),
options: { options: {
@@ -329,19 +369,20 @@
function changedCurrentQueryPart() { function changedCurrentQueryPart() {
if (queryParts.length <= 1) { if (queryParts.length <= 1) {
removeCurrentPartMarker(); removeCurrentPartMarker();
updateAnnotations();
return; return;
} }
const selectionRange = editor.getSelectionRange(); const selectionRange = editor.getSelectionRange();
if ( // if (
selectionRange.start.row != selectionRange.end.row || // selectionRange.start.row != selectionRange.end.row ||
selectionRange.start.column != selectionRange.end.column // selectionRange.start.column != selectionRange.end.column
) { // ) {
removeCurrentPartMarker(); // removeCurrentPartMarker();
currentPart = null; // currentPart = null;
return; // return;
} // }
const cursor = selectionRange.start; const cursor = selectionRange.start;
const part = queryParts.find( const part = queryParts.find(
@@ -350,25 +391,39 @@
((cursor.row == x.end.line && cursor.column <= x.end.column) || cursor.row < x.end.line) ((cursor.row == x.end.line && cursor.column <= x.end.column) || cursor.row < x.end.line)
); );
if (part?.text != currentPart?.text || part?.start?.position != currentPart?.start?.position) { if (
part?.text != currentPart?.text ||
part?.start?.position != currentPart?.start?.position ||
part?.end?.position != currentPart?.end?.position
) {
removeCurrentPartMarker(); removeCurrentPartMarker();
currentPart = part; currentPart = part;
if (currentPart) { if (currentPart) {
const start = currentPart.trimStart || currentPart.start; const start = currentPart.trimStart || currentPart.start;
const end = currentPart.trimEnd || currentPart.end; const end = currentPart.trimEnd || currentPart.end;
currentPartMarker = editor if (start && end) {
.getSession() currentPartLines = _.range(start.line, end.line + 1);
.addMarker(new ace.Range(start.line, start.column, end.line, end.column), 'ace_active-line', 'text'); for (const row of currentPartLines) {
if ((queryParts || []).find(part => part.trimStart.line == row)) {
continue;
} }
editor.getSession().addGutterDecoration(row, 'ace-gutter-current-part');
}
}
// currentPartMarker = editor
// .getSession()
// .addMarker(new ace.Range(start.line, start.column, end.line, end.column), 'ace_active-line', 'text');
}
updateAnnotations();
} }
} }
function removeCurrentPartMarker() { function removeCurrentPartMarker() {
if (currentPartMarker != null) { for (const row of currentPartLines) {
editor.getSession().removeMarker(currentPartMarker); editor.getSession().removeGutterDecoration(row, 'ace-gutter-current-part');
currentPartMarker = null;
} }
currentPartLines = [];
} }
onMount(() => { onMount(() => {
@@ -380,6 +435,7 @@
editor.getSession().setMode('ace/mode/' + mode); editor.getSession().setMode('ace/mode/' + mode);
editor.setTheme('ace/theme/' + theme); editor.setTheme('ace/theme/' + theme);
editor.setValue(value, 1); editor.setValue(value, 1);
editor.setHighlightActiveLine(false);
contentBackup = value; contentBackup = value;
setEventCallBacks(); setEventCallBacks();
if (options) { if (options) {
@@ -392,6 +448,77 @@
editor.container.addEventListener('contextmenu', handleContextMenu); editor.container.addEventListener('contextmenu', handleContextMenu);
editor.keyBinding.addKeyboardHandler(handleKeyDown); editor.keyBinding.addKeyboardHandler(handleKeyDown);
changedQueryParts(); changedQueryParts();
// editor.session.addGutterDecoration(0, 'ace-gutter-sql-run');
// editor.session.setAnnotations([
// {
// row: 1,
// text: 'SQL SCRIPT 0',
// type: 'gutter',
// },
// ]);
// editor.getSession().setAnnotations([
// {
// row: 0,
// // column: 0,
// text: 'Error Message', // Or the Json reply from the parser
// type: 'error', // also "warning" and "information"
// },
// {
// row: 1,
// // column: 0,
// text: 'SELECT * FROM \n22222', // Or the Json reply from the parser
// // type: 'info', // also "warning" and "information"
// className: 'ace-gutter-sql-run',
// },
// ]);
// editor.on('guttermousemove', e => console.log('MOVE', e.target), true);
// editor.on('guttermouseout', e => console.log('OUT', e.target), true);
// editor.on('guttermouseleave', e => console.log('LEAVE', e.target), true);
// editor.session.setBreakpoint(0);
// editor.on(
// 'gutterclick',
// e => {
// const row = e.getDocumentPosition().row;
// const part = (queryParts || []).find(part => part.trimStart.line == row);
// if (part && onExecuteFragment) {
// onExecuteFragment(part.text);
// e.stop();
// }
// },
// true
// );
editor.on(
'guttermousedown',
e => {
const row = e.getDocumentPosition().row;
const part = (queryParts || []).find(part => part.trimStart.line == row);
if (part && onExecuteFragment) {
onExecuteFragment(part.text, part.trimStart.line);
e.stop();
editor.moveCursorTo(part.trimStart.line, 0);
editor.selection.clearSelection();
}
},
true
);
// editor.session.gutterRenderer = {
// getWidth: function (session, lastLineNumber, config) {
// return lastLineNumber.toString().length * config.characterWidth;
// },
// getText: function (session, row) {
// return (row + 1).toString();
// // return String.fromCharCode(row + 65);
// },
// };
}); });
onDestroy(() => { onDestroy(() => {
@@ -427,6 +554,7 @@
editor.on('focus', () => dispatch('focus')); editor.on('focus', () => dispatch('focus'));
editor.setReadOnly(readOnly); editor.setReadOnly(readOnly);
editor.on('change', () => { editor.on('change', () => {
const content = editor.getValue(); const content = editor.getValue();
value = content; value = content;

View File

@@ -18,6 +18,7 @@
export let items: any[]; export let items: any[];
export let showProcedure = false; export let showProcedure = false;
export let showLine = false; export let showLine = false;
export let startLine = 0;
$: time0 = items[0] && new Date(items[0].time).getTime(); $: time0 = items[0] && new Date(items[0].time).getTime();
@@ -58,7 +59,7 @@
<td>{row.procedure || ''}</td> <td>{row.procedure || ''}</td>
{/if} {/if}
{#if showLine} {#if showLine}
<td>{row.line || ''}</td> <td>{row.line == null ? '' : row.line + 1 + startLine}</td>
{/if} {/if}
</tr> </tr>
{/each} {/each}

View File

@@ -13,8 +13,11 @@
export let eventName; export let eventName;
export let executeNumber; export let executeNumber;
export let showNoMessagesAlert = false; export let showNoMessagesAlert = false;
export let startLine = 0;
export let onChangeErrors = null;
const cachedMessagesRef = createRef([]); const cachedMessagesRef = createRef([]);
const lastErrorMessageCountRef = createRef(0);
let displayedMessages = []; let displayedMessages = [];
@@ -44,11 +47,26 @@
} }
} }
$: {
if (onChangeErrors) {
const errors = displayedMessages.filter(x => x.severity == 'error' && x.line != null);
if (lastErrorMessageCountRef.get() != errors.length) {
onChangeErrors(
errors.map(err => ({
...err,
line: err.line == null ? null : err.line + startLine,
}))
);
lastErrorMessageCountRef.set(errors.length);
}
}
}
$: $effect; $: $effect;
</script> </script>
{#if showNoMessagesAlert && (!displayedMessages || displayedMessages.length == 0)} {#if showNoMessagesAlert && (!displayedMessages || displayedMessages.length == 0)}
<ErrorInfo message="No messages" icon="img alert" /> <ErrorInfo message="No messages" icon="img alert" />
{:else} {:else}
<MessageView items={displayedMessages} on:messageclick {showProcedure} {showLine} /> <MessageView items={displayedMessages} on:messageclick {showProcedure} {showLine} {startLine} />
{/if} {/if}

View File

@@ -32,7 +32,7 @@
return domEditor.getEditor(); return domEditor.getEditor();
} }
export function getCurrentCommandText(): string { export function getCurrentCommandText(): { text: string; line?: number } {
return domEditor.getCurrentCommandText(); return domEditor.getCurrentCommandText();
} }

View File

@@ -2,6 +2,7 @@ import _ from 'lodash';
import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools'; import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
import { getDatabaseInfo } from '../utility/metadataLoaders'; import { getDatabaseInfo } from '../utility/metadataLoaders';
import analyseQuerySources from './analyseQuerySources'; import analyseQuerySources from './analyseQuerySources';
import { getStringSettingsValue } from '../settings/settingsTools';
const COMMON_KEYWORDS = [ const COMMON_KEYWORDS = [
'select', 'select',
@@ -78,13 +79,21 @@ export function mountCodeCompletion({ conid, database, editor, getText }) {
const line = session.getLine(cursor.row).slice(0, cursor.column); const line = session.getLine(cursor.row).slice(0, cursor.column);
const dbinfo = await getDatabaseInfo({ conid, database }); const dbinfo = await getDatabaseInfo({ conid, database });
let list = COMMON_KEYWORDS.map(word => ({ const convertUpper = getStringSettingsValue('sqlEditor.sqlCommandsCase', 'upperCase') == 'upperCase';
let list = COMMON_KEYWORDS.map(word => {
if (convertUpper) {
word = word.toUpperCase();
}
return {
name: word, name: word,
value: word, value: word,
caption: word, caption: word,
meta: 'keyword', meta: 'keyword',
score: 800, score: 800,
})); };
});
if (dbinfo) { if (dbinfo) {
const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/); const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);

View File

@@ -173,6 +173,10 @@
/> />
{/if} {/if}
{#if driver?.showConnectionField('treeKeySeparator', $values)}
<FormTextField label="Key separator" name="treeKeySeparator" disabled={isConnected} placeholder=":" />
{/if}
{#if driver?.showConnectionField('windowsDomain', $values)} {#if driver?.showConnectionField('windowsDomain', $values)}
<FormTextField label="Domain (specify to use NTLM authentication)" name="windowsDomain" disabled={isConnected} /> <FormTextField label="Domain (specify to use NTLM authentication)" name="windowsDomain" disabled={isConnected} />
{/if} {/if}

View File

@@ -111,6 +111,19 @@ ORDER BY
defaultValue="30" defaultValue="30"
disabled={values['connection.autoRefresh'] === false} disabled={values['connection.autoRefresh'] === false}
/> />
<div class="heading">SQL editor</div>
<FormSelectField
label="SQL commands case"
name="sqlEditor.sqlCommandsCase"
isNative
defaultValue="upperCase"
options={[
{ value: 'upperCase', label: 'UPPER CASE' },
{ value: 'lowerCase', label: 'lower case' },
]}
/>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="2"> <svelte:fragment slot="2">
<div class="heading">Application theme</div> <div class="heading">Application theme</div>

View File

@@ -21,3 +21,10 @@ export function getBoolSettingsValue(name, defaultValue) {
if (res == null) return defaultValue; if (res == null) return defaultValue;
return !!res; return !!res;
} }
export function getStringSettingsValue(name, defaultValue) {
const settings = getCurrentSettings();
const res = settings[name];
if (res == null) return defaultValue;
return res;
}

View File

@@ -52,7 +52,7 @@
import useEditorData from '../query/useEditorData'; import useEditorData from '../query/useEditorData';
import { extensions } from '../stores'; import { extensions } from '../stores';
import applyScriptTemplate from '../utility/applyScriptTemplate'; import applyScriptTemplate from '../utility/applyScriptTemplate';
import { changeTab } from '../utility/common'; import { changeTab, markTabUnsaved } from '../utility/common';
import { getDatabaseInfo, useConnectionInfo } from '../utility/metadataLoaders'; import { getDatabaseInfo, useConnectionInfo } from '../utility/metadataLoaders';
import SocketMessageView from '../query/SocketMessageView.svelte'; import SocketMessageView from '../query/SocketMessageView.svelte';
import useEffect from '../utility/useEffect'; import useEffect from '../utility/useEffect';
@@ -86,10 +86,11 @@
let busy = false; let busy = false;
let executeNumber = 0; let executeNumber = 0;
let executeStartLine = 0;
let visibleResultTabs = false; let visibleResultTabs = false;
let sessionId = null; let sessionId = null;
let resultCount; let resultCount;
let errorMessages;
let domEditor; let domEditor;
$: connection = useConnectionInfo({ conid }); $: connection = useConnectionInfo({ conid });
@@ -143,13 +144,14 @@
return !!conid && (!$connection?.isReadOnly || driver?.readOnlySessions); return !!conid && (!$connection?.isReadOnly || driver?.readOnlySessions);
} }
async function executeCore(sql) { async function executeCore(sql, startLine = 0) {
if (busy) return; if (busy) return;
if (!sql || !sql.trim()) { if (!sql || !sql.trim()) {
showSnackbarError('Skipped executing empty query'); showSnackbarError('Skipped executing empty query');
return; return;
} }
executeStartLine = startLine;
executeNumber++; executeNumber++;
visibleResultTabs = true; visibleResultTabs = true;
@@ -179,13 +181,14 @@
} }
export async function executeCurrent() { export async function executeCurrent() {
const sql = domEditor.getCurrentCommandText(); const cmd = domEditor.getCurrentCommandText();
await executeCore(sql); await executeCore(cmd.text, cmd.line);
} }
export async function execute() { export async function execute() {
const selectedText = domEditor.getEditor().getSelectedText(); const selectedText = domEditor.getEditor().getSelectedText();
await executeCore(selectedText || $editorValue); const startLine = domEditor.getEditor().getSelectionRange().start.row;
await executeCore(selectedText || $editorValue, selectedText ? startLine : 0);
} }
export async function kill() { export async function kill() {
@@ -257,6 +260,10 @@
: null, : null,
}); });
function handleChangeErrors(errors) {
errorMessages = errors;
}
function createMenu() { function createMenu() {
return [ return [
{ command: 'query.execute' }, { command: 'query.execute' },
@@ -276,6 +283,8 @@
} }
const quickExportHandlerRef = createQuickExportHandlerRef(); const quickExportHandlerRef = createQuickExportHandlerRef();
let isInitialized = false;
</script> </script>
<ToolStripContainer> <ToolStripContainer>
@@ -286,21 +295,32 @@
engine={$connection && $connection.engine} engine={$connection && $connection.engine}
{conid} {conid}
{database} {database}
splitterOptions={driver?.getQuerySplitterOptions('script')} splitterOptions={driver?.getQuerySplitterOptions('editor')}
value={$editorState.value || ''} value={$editorState.value || ''}
menu={createMenu()} menu={createMenu()}
on:input={e => setEditorData(e.detail)} on:input={e => {
setEditorData(e.detail);
if (isInitialized) {
markTabUnsaved(tabid);
}
errorMessages = [];
}}
on:focus={() => { on:focus={() => {
activator.activate(); activator.activate();
invalidateCommands(); invalidateCommands();
setTimeout(() => {
isInitialized = true;
}, 100);
}} }}
bind:this={domEditor} bind:this={domEditor}
onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)}
{errorMessages}
/> />
{:else} {:else}
<AceEditor <AceEditor
mode={driver?.editorMode || 'text'} mode={driver?.editorMode || 'text'}
value={$editorState.value || ''} value={$editorState.value || ''}
splitterOptions={driver?.getQuerySplitterOptions('script')} splitterOptions={driver?.getQuerySplitterOptions('editor')}
menu={createMenu()} menu={createMenu()}
on:input={e => setEditorData(e.detail)} on:input={e => setEditorData(e.detail)}
on:focus={() => { on:focus={() => {
@@ -318,8 +338,10 @@
eventName={sessionId ? `session-info-${sessionId}` : null} eventName={sessionId ? `session-info-${sessionId}` : null}
on:messageClick={handleMesageClick} on:messageClick={handleMesageClick}
{executeNumber} {executeNumber}
startLine={executeStartLine}
showProcedure showProcedure
showLine showLine
onChangeErrors={handleChangeErrors}
/> />
</svelte:fragment> </svelte:fragment>
</ResultTabs> </ResultTabs>

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import { SqlDumper } from 'dbgate-tools';
import { useSettings } from './metadataLoaders';
const settings = useSettings();
$: SqlDumper.keywordsCase = $settings?.['sqlEditor.sqlCommandsCase'] || 'upperCase';
</script>

View File

@@ -1,4 +1,4 @@
import { openedTabs } from '../stores'; import { getOpenedTabs, openedTabs } from '../stores';
import _ from 'lodash'; import _ from 'lodash';
import getElectron from './getElectron'; import getElectron from './getElectron';
@@ -18,6 +18,16 @@ export function changeTab(tabid, changeFunc) {
openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? changeFunc(tab) : tab))); openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? changeFunc(tab) : tab)));
} }
export function markTabUnsaved(tabid) {
const tab = getOpenedTabs().find(x => x.tabid == tabid);
if (tab.unsaved) return;
openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? { ...tab, unsaved: true } : tab)));
}
export function markTabSaved(tabid) {
openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? { ...tab, unsaved: false } : tab)));
}
export function setSelectedTabFunc(files, tabid) { export function setSelectedTabFunc(files, tabid) {
return [ return [
...(files || []).filter(x => x.tabid != tabid).map(x => ({ ...x, selected: false })), ...(files || []).filter(x => x.tabid != tabid).map(x => ({ ...x, selected: false })),

View File

@@ -1,7 +1,7 @@
import { derived, get } from 'svelte/store'; import { derived, get } from 'svelte/store';
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import { openedTabs } from '../stores'; import { openedTabs } from '../stores';
import { changeTab } from './common'; import { changeTab, markTabSaved } from './common';
import SaveFileModal from '../modals/SaveFileModal.svelte'; import SaveFileModal from '../modals/SaveFileModal.svelte';
import registerCommand from '../commands/registerCommand'; import registerCommand from '../commands/registerCommand';
import { apiCall } from './api'; import { apiCall } from './api';
@@ -24,12 +24,14 @@ export default async function saveTabFile(editor, saveMode, folder, format, file
if (savedFilePath) { if (savedFilePath) {
await apiCall('files/save-as', { filePath: savedFilePath, data, format }); await apiCall('files/save-as', { filePath: savedFilePath, data, format });
} }
markTabSaved(tabid);
}; };
const onSave = (title, newProps) => { const onSave = (title, newProps) => {
changeTab(tabid, tab => ({ changeTab(tabid, tab => ({
...tab, ...tab,
title, title,
unsaved: false,
props: { props: {
...tab.props, ...tab.props,
savedFormat: format, savedFormat: format,

View File

@@ -0,0 +1,125 @@
import {
analyseDataPattern,
MultipleDatabaseInfo,
PerspectiveCache,
PerspectiveConfig,
PerspectiveDatabaseConfig,
PerspectiveDataLoadProps,
PerspectiveDataPattern,
PerspectiveDataPatternDict,
} from 'dbgate-datalib';
import { PerspectiveDataLoader } from 'dbgate-datalib/lib/PerspectiveDataLoader';
import { writable, Readable } from 'svelte/store';
export function getPerspectiveDataPatternsFromCache(
databaseConfig: PerspectiveDatabaseConfig,
config: PerspectiveConfig,
cache: PerspectiveCache,
dbInfos: MultipleDatabaseInfo
): PerspectiveDataPatternDict {
const res = {};
for (const node of config.nodes) {
const conid = node.conid || databaseConfig.conid;
const database = node.database || databaseConfig.database;
const { schemaName, pureName } = node;
const cached = cache.dataPatterns.find(
x => x.conid == conid && x.database == database && x.schemaName == schemaName && x.pureName == pureName
);
if (cached) {
res[node.designerId] = cached;
}
}
return res;
}
export async function getPerspectiveDataPatterns(
databaseConfig: PerspectiveDatabaseConfig,
config: PerspectiveConfig,
cache: PerspectiveCache,
dbInfos: MultipleDatabaseInfo,
dataLoader: PerspectiveDataLoader
): Promise<PerspectiveDataPatternDict> {
const res = {};
for (const node of config.nodes) {
const conid = node.conid || databaseConfig.conid;
const database = node.database || databaseConfig.database;
const { schemaName, pureName } = node;
const cached = cache.dataPatterns.find(
x => x.conid == conid && x.database == database && x.schemaName == schemaName && x.pureName == pureName
);
if (cached) {
res[node.designerId] = cached;
continue;
}
const db = dbInfos?.[conid]?.[database];
if (!db) continue;
const table = db.tables?.find(x => x.pureName == pureName && x.schemaName == schemaName);
const view = db.views?.find(x => x.pureName == pureName && x.schemaName == schemaName);
const collection = db.collections?.find(x => x.pureName == pureName && x.schemaName == schemaName);
if (!table && !view && !collection) continue;
// console.log('LOAD PATTERN FOR', pureName);
const props: PerspectiveDataLoadProps = {
databaseConfig: { conid, database },
engineType: collection ? 'docdb' : 'sqldb',
schemaName,
pureName,
orderBy: table?.primaryKey
? table?.primaryKey.columns.map(x => ({ columnName: x.columnName, order: 'ASC' }))
: table || view
? [{ columnName: (table || view).columns[0].columnName, order: 'ASC' }]
: null,
range: {
offset: 0,
limit: 10,
},
};
// console.log('LOAD PROPS', props);
const rows = await dataLoader.loadData(props);
if (rows.errorMessage) {
console.error('Error loading pattern for', pureName, ':', rows.errorMessage);
continue;
}
// console.log('PATTERN ROWS', rows);
const pattern = analyseDataPattern(
{
conid,
database,
pureName,
schemaName,
},
rows
);
cache.dataPatterns.push(pattern);
res[node.designerId] = pattern;
}
return res;
}
export function usePerspectiveDataPatterns(
databaseConfig: PerspectiveDatabaseConfig,
config: PerspectiveConfig,
cache: PerspectiveCache,
dbInfos: MultipleDatabaseInfo,
dataLoader: PerspectiveDataLoader
): Readable<PerspectiveDataPatternDict> {
const cached = getPerspectiveDataPatternsFromCache(databaseConfig, config, cache, dbInfos);
const promise = getPerspectiveDataPatterns(databaseConfig, config, cache, dbInfos, dataLoader);
const res = writable(cached);
promise.then(value => res.set(value));
return res;
}

View File

@@ -1,5 +1,23 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
const closeTabFunc = closeCondition => tabid => { function allowCloseTabs(tabs) {
if (tabs.length == 0) return Promise.resolve(true);
return new Promise(resolve => {
showModal(CloseTabModal, {
onCancel: () => resolve(false),
onConfirm: () => resolve(true),
tabs,
});
});
}
const closeTabFunc = closeCondition => async tabid => {
const activeCandidate = getOpenedTabs().find(x => x.tabid == tabid);
const closeCandidates = getOpenedTabs()
.filter(x => closeCondition(x, activeCandidate))
.filter(x => x.unsaved && x.closedTime == null);
if (!(await allowCloseTabs(closeCandidates))) return;
openedTabs.update(files => { openedTabs.update(files => {
const active = files.find(x => x.tabid == tabid); const active = files.find(x => x.tabid == tabid);
if (!active) return files; if (!active) return files;
@@ -22,7 +40,13 @@
}); });
}; };
export const closeMultipleTabs = (closeCondition, deleteFromHistory = false) => { export const closeMultipleTabs = async (closeCondition, deleteFromHistory = false) => {
const closeCandidates = getOpenedTabs()
.filter(x => closeCondition(x))
.filter(x => x.unsaved && x.closedTime == null);
if (!(await allowCloseTabs(closeCandidates))) return;
openedTabs.update(files => { openedTabs.update(files => {
const newFiles = deleteFromHistory const newFiles = deleteFromHistory
? files.filter(x => !closeCondition(x)) ? files.filter(x => !closeCondition(x))
@@ -45,7 +69,11 @@
}; };
const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid); const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid);
const closeAll = () => { const closeAll = async () => {
const closeCandidates = getOpenedTabs().filter(x => x.unsaved && x.closedTime == null);
if (!(await allowCloseTabs(closeCandidates))) return;
const closedTime = new Date().getTime(); const closedTime = new Date().getTime();
openedTabs.update(tabs => openedTabs.update(tabs =>
tabs.map(tab => ({ tabs.map(tab => ({
@@ -212,6 +240,8 @@
import { getConnectionInfo, useConnectionList } from '../utility/metadataLoaders'; import { getConnectionInfo, useConnectionList } from '../utility/metadataLoaders';
import { duplicateTab, getTabDbKey, sortTabs, groupTabs } from '../utility/openNewTab'; import { duplicateTab, getTabDbKey, sortTabs, groupTabs } from '../utility/openNewTab';
import { useConnectionColorFactory } from '../utility/useConnectionColor'; import { useConnectionColorFactory } from '../utility/useConnectionColor';
import TabCloseButton from '../elements/TabCloseButton.svelte';
import CloseTabModal from '../modals/CloseTabModal.svelte';
$: connectionList = useConnectionList(); $: connectionList = useConnectionList();
@@ -434,7 +464,6 @@
<FontIcon icon="icon lock" /> <FontIcon icon="icon lock" />
{/if} {/if}
</div> </div>
<div <div
class="close-button-right tabCloseButton" class="close-button-right tabCloseButton"
on:click={e => closeMultipleTabs(tab => tabGroup.tabs.find(x => x.tabid == tab.tabid))} on:click={e => closeMultipleTabs(tab => tabGroup.tabs.find(x => x.tabid == tab.tabid))}
@@ -479,9 +508,7 @@
<span class="file-name"> <span class="file-name">
{tab.title} {tab.title}
</span> </span>
<span class="close-button tabCloseButton" on:click={e => closeTab(tab.tabid)}> <TabCloseButton unsaved={tab.unsaved} on:click={e => closeTab(tab.tabid)} />
<FontIcon icon="icon close" />
</span>
</div> </div>
{/each} {/each}
</div> </div>
@@ -582,19 +609,12 @@
white-space: nowrap; white-space: nowrap;
flex-grow: 1; flex-grow: 1;
} }
.close-button {
margin-left: 5px;
color: var(--theme-font-3);
}
.close-button-right { .close-button-right {
margin-left: 5px; margin-left: 5px;
margin-right: 5px; margin-right: 5px;
color: var(--theme-font-3); color: var(--theme-font-3);
} }
.close-button:hover {
color: var(--theme-font-1);
}
.close-button-right:hover { .close-button-right:hover {
color: var(--theme-font-1); color: var(--theme-font-1);
} }

View File

@@ -32,7 +32,7 @@
}, },
"devDependencies": { "devDependencies": {
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"webpack": "^4.42.0", "webpack": "^4.42.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",

View File

@@ -9,7 +9,9 @@ const AbstractCursor = require('mongodb').AbstractCursor;
const createBulkInsertStream = require('./createBulkInsertStream'); const createBulkInsertStream = require('./createBulkInsertStream');
function transformMongoData(row) { function transformMongoData(row) {
return _.mapValues(row, (v) => (v && v.constructor == ObjectId ? { $oid: v.toString() } : v)); return _.cloneDeepWith(row, (x) => {
if (x && x.constructor == ObjectId) return { $oid: x.toString() };
});
} }
async function readCursor(cursor, options) { async function readCursor(cursor, options) {
@@ -207,6 +209,10 @@ const driver = {
if (options.countDocuments) { if (options.countDocuments) {
const count = await collection.countDocuments(convertObjectId(options.condition) || {}); const count = await collection.countDocuments(convertObjectId(options.condition) || {});
return { count }; return { count };
} else if (options.aggregate) {
let cursor = await collection.aggregate(options.aggregate);
const rows = await cursor.toArray();
return { rows: rows.map(transformMongoData) };
} else { } else {
// console.log('options.condition', JSON.stringify(options.condition, undefined, 2)); // console.log('options.condition', JSON.stringify(options.condition, undefined, 2));
let cursor = await collection.find(convertObjectId(options.condition) || {}); let cursor = await collection.find(convertObjectId(options.condition) || {});
@@ -276,6 +282,11 @@ const driver = {
await db.createCollection('collection1'); await db.createCollection('collection1');
}, },
async dropDatabase(pool, name) {
const db = pool.db(name);
await db.dropDatabase();
},
async loadFieldValues(pool, name, field, search) { async loadFieldValues(pool, name, field, search) {
try { try {
const collection = pool.__getDatabase().collection(name.pureName); const collection = pool.__getDatabase().collection(name.pureName);

View File

@@ -32,7 +32,7 @@
}, },
"devDependencies": { "devDependencies": {
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"webpack": "^4.42.0", "webpack": "^4.42.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",

View File

@@ -134,7 +134,7 @@ async function tediousStream(pool, sql, options) {
const { message, lineNumber, procName } = info; const { message, lineNumber, procName } = info;
options.info({ options.info({
message, message,
line: lineNumber, line: lineNumber != null && lineNumber > 0 ? lineNumber - 1 : lineNumber,
procedure: procName, procedure: procName,
time: new Date(), time: new Date(),
severity: 'info', severity: 'info',
@@ -144,7 +144,7 @@ async function tediousStream(pool, sql, options) {
const { message, lineNumber, procName } = error; const { message, lineNumber, procName } = error;
options.info({ options.info({
message, message,
line: lineNumber, line: lineNumber != null && lineNumber > 0 ? lineNumber - 1 : lineNumber,
procedure: procName, procedure: procName,
time: new Date(), time: new Date(),
severity: 'error', severity: 'error',

View File

@@ -16,6 +16,16 @@ class MsSqlDumper extends SqlDumper {
} }
} }
dropDatabase(name) {
this.putCmd(
`USE master;
ALTER DATABASE %i SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE %i`,
name,
name
);
}
autoIncrement() { autoIncrement() {
this.put(' ^identity'); this.put(' ^identity');
} }

View File

@@ -10,6 +10,7 @@ const dialect = {
rangeSelect: true, rangeSelect: true,
offsetFetchRangeSyntax: true, offsetFetchRangeSyntax: true,
rowNumberOverPaging: true, rowNumberOverPaging: true,
defaultSchemaName: 'dbo',
stringEscapeChar: "'", stringEscapeChar: "'",
fallbackDataType: 'nvarchar(max)', fallbackDataType: 'nvarchar(max)',
explicitDropConstraint: false, explicitDropConstraint: false,
@@ -130,7 +131,10 @@ const driver = {
(field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi') || (field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi') ||
(field == 'windowsDomain' && values.authType != 'sql' && values.authType != 'sspi'), (field == 'windowsDomain' && values.authType != 'sql' && values.authType != 'sspi'),
// (field == 'useDatabaseUrl' && values.authType != 'sql' && values.authType != 'sspi') // (field == 'useDatabaseUrl' && values.authType != 'sql' && values.authType != 'sspi')
getQuerySplitterOptions: () => mssqlSplitterOptions, getQuerySplitterOptions: usage =>
usage == 'editor'
? { ...mssqlSplitterOptions, adaptiveGoSplit: true, ignoreComments: true, preventSingleLineSplit: true }
: mssqlSplitterOptions,
engine: 'mssql@dbgate-plugin-mssql', engine: 'mssql@dbgate-plugin-mssql',
title: 'Microsoft SQL Server', title: 'Microsoft SQL Server',

View File

@@ -33,7 +33,7 @@
"devDependencies": { "devDependencies": {
"antares-mysql-dumper": "^0.0.1", "antares-mysql-dumper": "^0.0.1",
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"webpack": "^4.42.0", "webpack": "^4.42.0",

View File

@@ -112,11 +112,10 @@ const drivers = driverBases.map(driverBase => ({
const handleError = error => { const handleError = error => {
console.log('ERROR', error); console.log('ERROR', error);
const { message, lineNumber, procName } = error; const { message } = error;
options.info({ options.info({
message, message,
line: lineNumber, line: 0,
procedure: procName,
time: new Date(), time: new Date(),
severity: 'error', severity: 'error',
}); });

View File

@@ -108,7 +108,11 @@ const mysqlDriverBase = {
dumperClass: Dumper, dumperClass: Dumper,
dialect, dialect,
defaultPort: 3306, defaultPort: 3306,
getQuerySplitterOptions: () => mysqlSplitterOptions, getQuerySplitterOptions: usage =>
usage == 'editor'
? { ...mysqlSplitterOptions, ignoreComments: true, preventSingleLineSplit: true }
: mysqlSplitterOptions,
readOnlySessions: true, readOnlySessions: true,
supportsDatabaseDump: true, supportsDatabaseDump: true,
authTypeLabel: 'Connection mode', authTypeLabel: 'Connection mode',

View File

@@ -31,7 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pg": "^8.7.1", "pg": "^8.7.1",

View File

@@ -147,10 +147,14 @@ const drivers = driverBases.map(driverBase => ({
query.on('error', error => { query.on('error', error => {
console.log('ERROR', error); console.log('ERROR', error);
const { message, lineNumber, procName } = error; const { message, position, procName } = error;
let line = null;
if (position) {
line = sql.substring(0, parseInt(position)).replace(/[^\n]/g, '').length;
}
options.info({ options.info({
message, message,
line: lineNumber, line,
procedure: procName, procedure: procName,
time: new Date(), time: new Date(),
severity: 'error', severity: 'error',

View File

@@ -26,6 +26,10 @@ class Dumper extends SqlDumper {
} }
} }
dropDatabase(name) {
this.putCmd('^drop ^database %i ^with(^force)', name);
}
dropRecreatedTempTable(tmptable) { dropRecreatedTempTable(tmptable) {
this.putCmd('^drop ^table %i ^cascade', tmptable); this.putCmd('^drop ^table %i ^cascade', tmptable);
} }

View File

@@ -8,6 +8,7 @@ const spatialTypes = ['GEOGRAPHY'];
const dialect = { const dialect = {
rangeSelect: true, rangeSelect: true,
ilike: true, ilike: true,
defaultSchemaName: 'public',
// stringEscapeChar: '\\', // stringEscapeChar: '\\',
stringEscapeChar: "'", stringEscapeChar: "'",
fallbackDataType: 'varchar', fallbackDataType: 'varchar',
@@ -105,7 +106,10 @@ const postgresDriverBase = {
dialect, dialect,
// showConnectionField: (field, values) => // showConnectionField: (field, values) =>
// ['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field), // ['server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase'].includes(field),
getQuerySplitterOptions: () => postgreSplitterOptions, getQuerySplitterOptions: usage =>
usage == 'editor'
? { ...postgreSplitterOptions, ignoreComments: true, preventSingleLineSplit: true }
: postgreSplitterOptions,
readOnlySessions: true, readOnlySessions: true,
databaseUrlPlaceholder: 'e.g. postgresql://user:password@localhost:5432/default_database', databaseUrlPlaceholder: 'e.g. postgresql://user:password@localhost:5432/default_database',

View File

@@ -30,7 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"webpack": "^4.42.0", "webpack": "^4.42.0",

View File

@@ -81,7 +81,7 @@ function splitCommandLine(str) {
const driver = { const driver = {
...driverBase, ...driverBase,
analyserClass: Analyser, analyserClass: Analyser,
async connect({ server, port, password, database, useDatabaseUrl, databaseUrl }) { async connect({ server, port, password, database, useDatabaseUrl, databaseUrl, treeKeySeparator }) {
let db = 0; let db = 0;
let pool; let pool;
if (useDatabaseUrl) { if (useDatabaseUrl) {
@@ -95,6 +95,7 @@ const driver = {
password, password,
db, db,
}); });
pool.__treeKeySeparator = treeKeySeparator || ':';
} }
return pool; return pool;
@@ -164,9 +165,9 @@ const driver = {
}, },
async loadKeys(pool, root = '', filter = null) { async loadKeys(pool, root = '', filter = null) {
const keys = await this.getKeys(pool, root ? `${root}:*` : '*'); const keys = await this.getKeys(pool, root ? `${root}${pool.__treeKeySeparator}*` : '*');
const keysFiltered = keys.filter((x) => filterName(filter, x)); const keysFiltered = keys.filter((x) => filterName(filter, x));
const res = this.extractKeysFromLevel(root, keysFiltered); const res = this.extractKeysFromLevel(pool, root, keysFiltered);
await this.enrichKeyInfo(pool, res); await this.enrichKeyInfo(pool, res);
return res; return res;
}, },
@@ -196,13 +197,13 @@ const driver = {
return res; return res;
}, },
extractKeysFromLevel(root, keys) { extractKeysFromLevel(pool, root, keys) {
const prefix = root ? `${root}:` : ''; const prefix = root ? `${root}${pool.__treeKeySeparator}` : '';
const rootSplit = _.compact(root.split(':')); const rootSplit = _.compact(root.split(pool.__treeKeySeparator));
const res = {}; const res = {};
for (const key of keys) { for (const key of keys) {
if (!key.startsWith(prefix)) continue; if (!key.startsWith(prefix)) continue;
const keySplit = key.split(':'); const keySplit = key.split(pool.__treeKeySeparator);
if (keySplit.length > rootSplit.length) { if (keySplit.length > rootSplit.length) {
const text = keySplit[rootSplit.length]; const text = keySplit[rootSplit.length];
if (keySplit.length == rootSplit.length + 1) { if (keySplit.length == rootSplit.length + 1) {
@@ -216,9 +217,9 @@ const driver = {
res[dctKey].count++; res[dctKey].count++;
} else { } else {
res[dctKey] = { res[dctKey] = {
text: text + ':*', text: text + pool.__treeKeySeparator + '*',
type: 'dir', type: 'dir',
root: keySplit.slice(0, rootSplit.length + 1).join(':'), root: keySplit.slice(0, rootSplit.length + 1).join(pool.__treeKeySeparator),
count: 1, count: 1,
}; };
} }

View File

@@ -78,9 +78,9 @@ const driver = {
showConnectionField: (field, values) => { showConnectionField: (field, values) => {
if (field == 'useDatabaseUrl') return true; if (field == 'useDatabaseUrl') return true;
if (values.useDatabaseUrl) { if (values.useDatabaseUrl) {
return ['databaseUrl', 'isReadOnly'].includes(field); return ['databaseUrl', 'isReadOnly', 'treeKeySeparator'].includes(field);
} }
return ['server', 'port', 'password', 'isReadOnly'].includes(field); return ['server', 'port', 'password', 'isReadOnly', 'treeKeySeparator'].includes(field);
}, },
showConnectionTab: (field) => field == 'sshTunnel', showConnectionTab: (field) => field == 'sshTunnel',

View File

@@ -32,7 +32,7 @@
"devDependencies": { "devDependencies": {
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",
"dbgate-plugin-tools": "^1.0.4", "dbgate-plugin-tools": "^1.0.4",
"dbgate-query-splitter": "^4.9.0", "dbgate-query-splitter": "^4.9.2",
"byline": "^5.0.0", "byline": "^5.0.0",
"webpack": "^4.42.0", "webpack": "^4.42.0",
"webpack-cli": "^3.3.11" "webpack-cli": "^3.3.11"

View File

@@ -104,10 +104,10 @@ const driver = {
inTransaction(); inTransaction();
} catch (error) { } catch (error) {
console.log('ERROR', error); console.log('ERROR', error);
const { message, lineNumber, procName } = error; const { message, procName } = error;
options.info({ options.info({
message, message,
line: lineNumber, line: 0,
procedure: procName, procedure: procName,
time: new Date(), time: new Date(),
severity: 'error', severity: 'error',

View File

@@ -50,7 +50,14 @@ const driver = {
singleDatabase: true, singleDatabase: true,
defaultDatabase: getDatabaseFileLabel(connection.databaseFile), defaultDatabase: getDatabaseFileLabel(connection.databaseFile),
}), }),
getQuerySplitterOptions: (usage) => (usage == 'stream' ? noSplitSplitterOptions : sqliteSplitterOptions),
getQuerySplitterOptions: (usage) =>
usage == 'editor'
? { ...sqliteSplitterOptions, ignoreComments: true, preventSingleLineSplit: true }
: usage == 'stream'
? noSplitSplitterOptions
: sqliteSplitterOptions,
// isFileDatabase: true, // isFileDatabase: true,
isElectronOnly: true, isElectronOnly: true,

View File

@@ -3309,10 +3309,10 @@ dbgate-plugin-xml@^5.0.0-alpha.1:
resolved "https://registry.yarnpkg.com/dbgate-plugin-xml/-/dbgate-plugin-xml-5.0.9.tgz#c3abf6ed8cd1450c45058d35c9326458833ed27e" resolved "https://registry.yarnpkg.com/dbgate-plugin-xml/-/dbgate-plugin-xml-5.0.9.tgz#c3abf6ed8cd1450c45058d35c9326458833ed27e"
integrity sha512-P8Em1A6HhF0BfxEDDEUyzdgFeJHEC5vbg12frANpWHjO3V1HGdygsT2z1ukLK8FS5BLW/vcCdOFldXZGh+wWvg== integrity sha512-P8Em1A6HhF0BfxEDDEUyzdgFeJHEC5vbg12frANpWHjO3V1HGdygsT2z1ukLK8FS5BLW/vcCdOFldXZGh+wWvg==
dbgate-query-splitter@^4.9.0: dbgate-query-splitter@^4.9.2:
version "4.9.0" version "4.9.2"
resolved "https://registry.yarnpkg.com/dbgate-query-splitter/-/dbgate-query-splitter-4.9.0.tgz#37475929b76ebe60436fcc44f223d4d47d6483af" resolved "https://registry.yarnpkg.com/dbgate-query-splitter/-/dbgate-query-splitter-4.9.2.tgz#ab1a60e60887ca750dd263a59db66e82c6461a46"
integrity sha512-POifNiMDkeksA9YXaC82u5O6krYC21xyROoNjDh3ouKI4xeB37DG+cP/D4IdICWHYZudlgKiziQ4v3W+5+O1DA== integrity sha512-MwZzNNLILdUv8rg6mFysLizIEdZsLLHEOL4lAHrvLPtHaLOAb275ogtgieLqjcnsXkPlV03i2t1b697aQYdfLQ==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3: debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9" version "2.6.9"