Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions spec/SqlSynonym.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {OpenDataQueryFormatter, QueryExpression, SqlFormatter, SqlSynonym} from '../src/index';

describe('SqlSynonym', () => {
const synonyms = SqlSynonym.getInstance();

beforeEach(() => {
synonyms.clear();
});

afterAll(() => {
synonyms.clear();
});

it('should format query by using SQL synonym', () => {
synonyms.set('Products', 'sales.Products');
const formatter = new SqlFormatter();
formatter.settings.nameFormat = '`$1`';
const query = new QueryExpression().select('id', 'name')
.from('Products').where('id').equal(100);
const sql = formatter.formatSelect(query);
const expectedSql = 'SELECT `sales`.`Products`.`id`, `sales`.`Products`.`name` FROM `sales`.`Products` WHERE (`id`=100)';
expect(sql).toBe(expectedSql);
});

it('should format qualified object names by using synonym', () => {
synonyms.set('Products', 'Production.Product');
const formatter = new SqlFormatter();
formatter.settings.nameFormat = '`$1`';
expect(formatter.escapeEntity('Products')).toBe('`Production`.`Product`');
expect(formatter.escapeName('Products.ProductID')).toBe('`Production`.`Product`.`ProductID`');
});

it('should apply synonyms in formatter subclasses', () => {
synonyms.set('Products', 'sales.Products');
const formatter = new OpenDataQueryFormatter();
expect(formatter.escapeName('Products.id')).toBe('sales/Products/id');
});

it('should prioritize formatter instance synonyms over static resolving handlers', () => {
synonyms.set('Products', 'sales.Products');
const formatter = new SqlFormatter();
formatter.settings.nameFormat = '`$1`';
formatter.synonyms = new SqlSynonym([
['Products', 'inventory.Products']
]);
expect(formatter.escapeEntity('Products')).toBe('`inventory`.`Products`');
});

it('should construct synonyms from array entries', () => {
const localSynonyms = new SqlSynonym([
['synonym1', 'schema1.object1'],
['synonym2', 'schema2.object2']
]);
expect(localSynonyms.get('synonym1')).toBe('schema1.object1');
expect(localSynonyms.get('synonym2')).toBe('schema2.object2');
});
});
4 changes: 4 additions & 0 deletions src/formatter.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// MOST Web Framework Codename Zero Gravity Copyright (c) 2017-2022, THEMOST LP All rights reserved
import {QueryEntity, QueryExpression, QueryField, QueryValueRef} from './query';
import {SyncSeriesEventEmitter} from '@themost/events';

export declare interface FormatterSettings {
nameFormat: string;
Expand All @@ -10,7 +11,10 @@ export declare interface FormatterSettings {
export type QueryToken = string | any;

export declare class SqlFormatter {
static resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string }>;
provider: any;
synonyms: { resolve(name: string): string } | null;
resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string }>;
settings: FormatterSettings;

escape(value: any,unquoted?: boolean): string | any;
Expand Down
38 changes: 28 additions & 10 deletions src/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ObjectNameValidator } from './object-name.validator';
import {isMethodOrNameReference, isNameReference, trimNameReference} from './name-reference';
import { JSONArray, JSONObject } from '@themost/json';
import {MethodCallExpression} from './expressions';
import {SyncSeriesEventEmitter} from '@themost/events';

class AbstractMethodError extends Error {
constructor() {
Expand All @@ -36,6 +37,17 @@ function getAliasKeyword() {
return ' ';
}

function resolveName(formatter, name) {
let str = name;
if (isNameReference(str)) {
str = trimNameReference(name);
}
if (formatter.synonyms && typeof formatter.synonyms.resolve === 'function') {
str = formatter.synonyms.resolve(str);
}
return str;
}

/**
* Initializes an SQL formatter class.
* @class SqlFormatter
Expand All @@ -45,6 +57,8 @@ class SqlFormatter {
constructor() {
//
this.provider = null;
this.synonyms = null;
this.resolvingName = SqlFormatter.resolvingName;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SqlFormatter should have an option to use their own SqlSynonym instance. This instance, if exists, might be used before using the static SqlFormatter.resolvingName

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 176ef4e (refined in ecf410f): SqlFormatter now supports a per-instance synonyms resolver, and it is applied before static resolvingName handlers. Added coverage to verify instance-level synonyms take precedence. No UI changes (screenshot N/A).

/**
* Gets or sets formatter settings
* @type {{nameFormat: string, forceAlias: boolean, useAliasKeyword: boolean}|*}
Expand Down Expand Up @@ -1093,22 +1107,24 @@ class SqlFormatter {
if (typeof name !== 'string') {
throw new Error('Invalid name expression. Expected string.');
}
let str = name;
if (isNameReference(str)) {
str = trimNameReference(name);
}
return ObjectNameValidator.validator.escape(str, this.settings.nameFormat);
const event = {
target: this,
name: resolveName(this, name)
};
this.resolvingName.emit(event);
return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat);
}

escapeEntity(name) {
if (typeof name !== 'string') {
throw new Error('Invalid entity expression. Expected string.');
}
let str = name;
if (isNameReference(str)) {
str = trimNameReference(name);
}
return ObjectNameValidator.validator.escape(str, this.settings.nameFormat);
const event = {
target: this,
name: resolveName(this, name)
};
this.resolvingName.emit(event);
return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat);
}

/**
Expand Down Expand Up @@ -1638,6 +1654,8 @@ class SqlFormatter {

}

SqlFormatter.resolvingName = new SyncSeriesEventEmitter();

export {
SqlFormatter
}
2 changes: 1 addition & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export * from './closures/StringMethodParser';
export * from './object-name.validator';
export * from './open-data-query.expression';
export * from './open-data-query.formatter';

export * from './sql-synonym';
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export * from './closures/DateMethodParser';
export * from './closures/StringMethodParser';
export * from './object-name.validator';
export * from './open-data-query.expression';
export * from './open-data-query.formatter';
export * from './open-data-query.formatter';
export * from './sql-synonym';
6 changes: 6 additions & 0 deletions src/sql-synonym.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export declare class SqlSynonym extends Map<string, string> {
constructor();
constructor(entries?: readonly (readonly [string, string])[] | null);
resolve(name: string): string;
static getInstance(): SqlSynonym;
}
72 changes: 72 additions & 0 deletions src/sql-synonym.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {SqlFormatter} from './formatter';

class SqlSynonym extends Map {
constructor(entries) {
super(entries);
}

set(synonym, name) {
if (typeof synonym !== 'string') {
throw new TypeError('Invalid synonym name. Expected string.');
}
if (typeof name !== 'string') {
throw new TypeError('Invalid target name. Expected string.');
}
return super.set(synonym, name);
}

get(synonym) {
return super.get(synonym);
}

delete(synonym) {
if (typeof synonym !== 'string') {
throw new TypeError('Invalid synonym name. Expected string.');
}
return super.delete(synonym);
}

clear() {
return super.clear();
}

resolve(name) {
if (typeof name !== 'string') {
throw new TypeError('Invalid synonym expression. Expected string.');
}
const exactMatch = this.get(name);
if (typeof exactMatch === 'string') {
return exactMatch;
}
let candidate;
for (const key of this.keys()) {
if (name.startsWith(key + '.')) {
if (candidate === undefined || key.length > candidate.length) {
candidate = key;
}
}
}
if (typeof candidate === 'string') {
return this.get(candidate).concat(name.substring(candidate.length));
}
return name;
}

static getInstance() {
if (this.instance === undefined) {
this.instance = new SqlSynonym();
}
return this.instance;
}
}

SqlFormatter.resolvingName.subscribe((event) => {
if (typeof event.name !== 'string') {
return;
}
event.name = SqlSynonym.getInstance().resolve(event.name);
});

export {
SqlSynonym
};