From 1ffb2e873e69c67588b8a87033aa5d0ef3a3ee17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:03:56 +0000 Subject: [PATCH 01/14] Initial plan From 13524ec1b6508c66cc609584ee2be7a5d6315f4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:08:55 +0000 Subject: [PATCH 02/14] feat: add SqlFormatter synonym resolution hooks Agent-Logs-Url: https://github.com/themost-framework/query/sessions/48a4fe49-749a-4721-b741-95974aa979b9 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 35 ++++++++++++++++++++++++++ src/formatter.d.ts | 3 +++ src/formatter.js | 22 +++++++++++++++-- src/index.d.ts | 2 +- src/index.js | 3 ++- src/sql-synonym.d.ts | 7 ++++++ src/sql-synonym.js | 55 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 spec/SqlSynonym.spec.js create mode 100644 src/sql-synonym.d.ts create mode 100644 src/sql-synonym.js diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js new file mode 100644 index 0000000..0f25490 --- /dev/null +++ b/spec/SqlSynonym.spec.js @@ -0,0 +1,35 @@ +import {OpenDataQueryFormatter, QueryExpression, SqlFormatter, SqlSynonym} from '../src/index'; + +describe('SqlSynonym', () => { + beforeEach(() => { + SqlSynonym.clear(); + }); + + afterAll(() => { + SqlSynonym.clear(); + }); + + it('should format query by using data object synonym', () => { + SqlSynonym.add('ProductData', 'MyProduct'); + const formatter = new SqlFormatter(); + formatter.settings.nameFormat = '`$1`'; + const query = new QueryExpression().select('id', 'name') + .from('ProductData').where('id').equal(100); + const sql = formatter.formatSelect(query); + expect(sql).toBe('SELECT `MyProduct`.`id`, `MyProduct`.`name` FROM `MyProduct` WHERE (`id`=100)'); + }); + + it('should format qualified object names by using synonym', () => { + SqlSynonym.add('Production.Product', 'MyProduct'); + const formatter = new SqlFormatter(); + formatter.settings.nameFormat = '`$1`'; + expect(formatter.escapeEntity('Production.Product')).toBe('`MyProduct`'); + expect(formatter.escapeName('Production.Product.ProductID')).toBe('`MyProduct`.`ProductID`'); + }); + + it('should apply synonyms in formatter subclasses', () => { + SqlSynonym.add('Products', 'MyProducts'); + const formatter = new OpenDataQueryFormatter(); + expect(formatter.escapeName('Products.id')).toBe('MyProducts/id'); + }); +}); diff --git a/src/formatter.d.ts b/src/formatter.d.ts index 520b50a..fd74c51 100644 --- a/src/formatter.d.ts +++ b/src/formatter.d.ts @@ -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; @@ -10,7 +11,9 @@ export declare interface FormatterSettings { export type QueryToken = string | any; export declare class SqlFormatter { + static resolvingName: SyncSeriesEventEmitter<{ formatter: SqlFormatter, name: string, format: string, type: string }>; provider: any; + resolvingName: SyncSeriesEventEmitter<{ formatter: SqlFormatter, name: string, format: string, type: string }>; settings: FormatterSettings; escape(value: any,unquoted?: boolean): string | any; diff --git a/src/formatter.js b/src/formatter.js index 3f6ac63..812f4d2 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -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() { @@ -45,6 +46,7 @@ class SqlFormatter { constructor() { // this.provider = null; + this.resolvingName = SqlFormatter.resolvingName; /** * Gets or sets formatter settings * @type {{nameFormat: string, forceAlias: boolean, useAliasKeyword: boolean}|*} @@ -1097,7 +1099,14 @@ class SqlFormatter { if (isNameReference(str)) { str = trimNameReference(name); } - return ObjectNameValidator.validator.escape(str, this.settings.nameFormat); + const event = { + formatter: this, + name: str, + format: this.settings.nameFormat, + type: 'name' + }; + this.resolvingName.emit(event); + return ObjectNameValidator.validator.escape(event.name, event.format); } escapeEntity(name) { @@ -1108,7 +1117,14 @@ class SqlFormatter { if (isNameReference(str)) { str = trimNameReference(name); } - return ObjectNameValidator.validator.escape(str, this.settings.nameFormat); + const event = { + formatter: this, + name: str, + format: this.settings.nameFormat, + type: 'entity' + }; + this.resolvingName.emit(event); + return ObjectNameValidator.validator.escape(event.name, event.format); } /** @@ -1638,6 +1654,8 @@ class SqlFormatter { } +SqlFormatter.resolvingName = new SyncSeriesEventEmitter(); + export { SqlFormatter } diff --git a/src/index.d.ts b/src/index.d.ts index adb4f93..2f635aa 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -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'; diff --git a/src/index.js b/src/index.js index 64c1c03..2f635aa 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; \ No newline at end of file +export * from './open-data-query.formatter'; +export * from './sql-synonym'; diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts new file mode 100644 index 0000000..0d887e0 --- /dev/null +++ b/src/sql-synonym.d.ts @@ -0,0 +1,7 @@ +export declare class SqlSynonym { + static add(name: string, synonym: string): void; + static remove(name: string): boolean; + static clear(): void; + static get(name: string): string | undefined; + static resolve(name: string): string; +} diff --git a/src/sql-synonym.js b/src/sql-synonym.js new file mode 100644 index 0000000..3e0c886 --- /dev/null +++ b/src/sql-synonym.js @@ -0,0 +1,55 @@ +import {SqlFormatter} from './formatter'; + +const synonyms = new Map(); + +class SqlSynonym { + static add(name, synonym) { + if (typeof name !== 'string') { + throw new TypeError('Invalid object name. Expected string.'); + } + if (typeof synonym !== 'string') { + throw new TypeError('Invalid synonym name. Expected string.'); + } + synonyms.set(name, synonym); + } + + static remove(name) { + if (typeof name !== 'string') { + throw new TypeError('Invalid object name. Expected string.'); + } + return synonyms.delete(name); + } + + static clear() { + synonyms.clear(); + } + + static get(name) { + return synonyms.get(name); + } + + static resolve(name) { + const exactMatch = synonyms.get(name); + if (typeof exactMatch === 'string') { + return exactMatch; + } + const keys = Array.from(synonyms.keys()).sort((a, b) => b.length - a.length); + for (const key of keys) { + if (name.startsWith(key + '.')) { + return synonyms.get(key).concat(name.substring(key.length)); + } + } + return name; + } +} + +SqlFormatter.resolvingName.subscribe((event) => { + if (typeof event.name !== 'string') { + return; + } + event.name = SqlSynonym.resolve(event.name); +}); + +export { + SqlSynonym +}; From 6e84bfbe3a56f37d18da3f34ddd92f50b9d073bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:32:26 +0000 Subject: [PATCH 03/14] refactor: adjust resolvingName event payload fields Agent-Logs-Url: https://github.com/themost-framework/query/sessions/648745f7-74b7-49f0-9fa1-44f2cb8aad06 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- src/formatter.d.ts | 4 ++-- src/formatter.js | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/formatter.d.ts b/src/formatter.d.ts index fd74c51..5e9619e 100644 --- a/src/formatter.d.ts +++ b/src/formatter.d.ts @@ -11,9 +11,9 @@ export declare interface FormatterSettings { export type QueryToken = string | any; export declare class SqlFormatter { - static resolvingName: SyncSeriesEventEmitter<{ formatter: SqlFormatter, name: string, format: string, type: string }>; + static resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string, type: string }>; provider: any; - resolvingName: SyncSeriesEventEmitter<{ formatter: SqlFormatter, name: string, format: string, type: string }>; + resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string, type: string }>; settings: FormatterSettings; escape(value: any,unquoted?: boolean): string | any; diff --git a/src/formatter.js b/src/formatter.js index 812f4d2..8ebf2eb 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -1100,13 +1100,12 @@ class SqlFormatter { str = trimNameReference(name); } const event = { - formatter: this, + target: this, name: str, - format: this.settings.nameFormat, type: 'name' }; this.resolvingName.emit(event); - return ObjectNameValidator.validator.escape(event.name, event.format); + return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); } escapeEntity(name) { @@ -1118,13 +1117,12 @@ class SqlFormatter { str = trimNameReference(name); } const event = { - formatter: this, + target: this, name: str, - format: this.settings.nameFormat, type: 'entity' }; this.resolvingName.emit(event); - return ObjectNameValidator.validator.escape(event.name, event.format); + return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); } /** From da5be426941ba213845ec47fc6eb1c4673a05fcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:49:38 +0000 Subject: [PATCH 04/14] refactor: simplify resolving event and map-based SqlSynonym service Agent-Logs-Url: https://github.com/themost-framework/query/sessions/b7f59e89-589e-4a24-89b7-34005331d980 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 9 +++++ src/formatter.d.ts | 4 +-- src/formatter.js | 6 ++-- src/sql-synonym.d.ts | 9 +++-- src/sql-synonym.js | 78 ++++++++++++++++++++++++++++++++--------- 5 files changed, 82 insertions(+), 24 deletions(-) diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js index 0f25490..8b999d8 100644 --- a/spec/SqlSynonym.spec.js +++ b/spec/SqlSynonym.spec.js @@ -32,4 +32,13 @@ describe('SqlSynonym', () => { const formatter = new OpenDataQueryFormatter(); expect(formatter.escapeName('Products.id')).toBe('MyProducts/id'); }); + + it('should add synonyms from array entries', () => { + SqlSynonym.add([ + ['object1', 'synonym1'], + ['object2', 'synonym2'] + ]); + expect(SqlSynonym.get('object1')).toBe('synonym1'); + expect(SqlSynonym.get('object2')).toBe('synonym2'); + }); }); diff --git a/src/formatter.d.ts b/src/formatter.d.ts index 5e9619e..f4dd98a 100644 --- a/src/formatter.d.ts +++ b/src/formatter.d.ts @@ -11,9 +11,9 @@ export declare interface FormatterSettings { export type QueryToken = string | any; export declare class SqlFormatter { - static resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string, type: string }>; + static resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string }>; provider: any; - resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string, type: string }>; + resolvingName: SyncSeriesEventEmitter<{ target: SqlFormatter, name: string }>; settings: FormatterSettings; escape(value: any,unquoted?: boolean): string | any; diff --git a/src/formatter.js b/src/formatter.js index 8ebf2eb..40e0f2d 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -1101,8 +1101,7 @@ class SqlFormatter { } const event = { target: this, - name: str, - type: 'name' + name: str }; this.resolvingName.emit(event); return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); @@ -1118,8 +1117,7 @@ class SqlFormatter { } const event = { target: this, - name: str, - type: 'entity' + name: str }; this.resolvingName.emit(event); return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts index 0d887e0..ddb7ac1 100644 --- a/src/sql-synonym.d.ts +++ b/src/sql-synonym.d.ts @@ -1,5 +1,10 @@ -export declare class SqlSynonym { - static add(name: string, synonym: string): void; +export declare class SqlSynonym extends Map { + add(name: string, synonym: string): SqlSynonym; + add(entries: [string, string][]): SqlSynonym; + resolve(name: string): string; + static getInstance(): SqlSynonym; + static add(name: string, synonym: string): SqlSynonym; + static add(entries: [string, string][]): SqlSynonym; static remove(name: string): boolean; static clear(): void; static get(name: string): string | undefined; diff --git a/src/sql-synonym.js b/src/sql-synonym.js index 3e0c886..502f86c 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -1,53 +1,99 @@ import {SqlFormatter} from './formatter'; -const synonyms = new Map(); - -class SqlSynonym { - static add(name, synonym) { +class SqlSynonym extends Map { + add(name, synonym) { + if (Array.isArray(name)) { + for (const item of name) { + if (Array.isArray(item) === false || item.length !== 2) { + throw new TypeError('Invalid synonym entry. Expected [name, synonym].'); + } + this.set(item[0], item[1]); + } + return this; + } if (typeof name !== 'string') { throw new TypeError('Invalid object name. Expected string.'); } if (typeof synonym !== 'string') { throw new TypeError('Invalid synonym name. Expected string.'); } - synonyms.set(name, synonym); + this.set(name, synonym); + return this; } - static remove(name) { + set(name, synonym) { if (typeof name !== 'string') { throw new TypeError('Invalid object name. Expected string.'); } - return synonyms.delete(name); + if (typeof synonym !== 'string') { + throw new TypeError('Invalid synonym name. Expected string.'); + } + return super.set(name, synonym); } - static clear() { - synonyms.clear(); + get(name) { + if (typeof name !== 'string') { + throw new TypeError('Invalid object name. Expected string.'); + } + return super.get(name); } - static get(name) { - return synonyms.get(name); + delete(name) { + if (typeof name !== 'string') { + throw new TypeError('Invalid object name. Expected string.'); + } + return super.delete(name); } - static resolve(name) { - const exactMatch = synonyms.get(name); + resolve(name) { + if (typeof name !== 'string') { + throw new TypeError('Invalid object name. Expected string.'); + } + const exactMatch = this.get(name); if (typeof exactMatch === 'string') { return exactMatch; } - const keys = Array.from(synonyms.keys()).sort((a, b) => b.length - a.length); + const keys = Array.from(this.keys()).sort((a, b) => b.length - a.length); for (const key of keys) { if (name.startsWith(key + '.')) { - return synonyms.get(key).concat(name.substring(key.length)); + return this.get(key).concat(name.substring(key.length)); } } return name; } + + static getInstance() { + return this.instance; + } + + static add(name, synonym) { + return this.getInstance().add(name, synonym); + } + + static remove(name) { + return this.getInstance().delete(name); + } + + static clear() { + this.getInstance().clear(); + } + + static get(name) { + return this.getInstance().get(name); + } + + static resolve(name) { + return this.getInstance().resolve(name); + } } +SqlSynonym.instance = new SqlSynonym(); + SqlFormatter.resolvingName.subscribe((event) => { if (typeof event.name !== 'string') { return; } - event.name = SqlSynonym.resolve(event.name); + event.name = SqlSynonym.getInstance().resolve(event.name); }); export { From 0d640d010e817349f96db3a0c85d24dab8da5f28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:50:44 +0000 Subject: [PATCH 05/14] style: address follow-up review in SqlSynonym Agent-Logs-Url: https://github.com/themost-framework/query/sessions/b7f59e89-589e-4a24-89b7-34005331d980 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- src/sql-synonym.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sql-synonym.js b/src/sql-synonym.js index 502f86c..d6fcd0b 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -4,7 +4,7 @@ class SqlSynonym extends Map { add(name, synonym) { if (Array.isArray(name)) { for (const item of name) { - if (Array.isArray(item) === false || item.length !== 2) { + if (!Array.isArray(item) || item.length !== 2) { throw new TypeError('Invalid synonym entry. Expected [name, synonym].'); } this.set(item[0], item[1]); @@ -46,9 +46,6 @@ class SqlSynonym extends Map { } resolve(name) { - if (typeof name !== 'string') { - throw new TypeError('Invalid object name. Expected string.'); - } const exactMatch = this.get(name); if (typeof exactMatch === 'string') { return exactMatch; From 3179dfbe67145648769db712ac481d855918db60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:51:56 +0000 Subject: [PATCH 06/14] fix: harden SqlSynonym singleton and d.ts add signatures Agent-Logs-Url: https://github.com/themost-framework/query/sessions/b7f59e89-589e-4a24-89b7-34005331d980 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- src/sql-synonym.d.ts | 6 ++---- src/sql-synonym.js | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts index ddb7ac1..7142e34 100644 --- a/src/sql-synonym.d.ts +++ b/src/sql-synonym.d.ts @@ -1,10 +1,8 @@ export declare class SqlSynonym extends Map { - add(name: string, synonym: string): SqlSynonym; - add(entries: [string, string][]): SqlSynonym; + add(name: string | [string, string][], synonym?: string): SqlSynonym; resolve(name: string): string; static getInstance(): SqlSynonym; - static add(name: string, synonym: string): SqlSynonym; - static add(entries: [string, string][]): SqlSynonym; + static add(name: string | [string, string][], synonym?: string): SqlSynonym; static remove(name: string): boolean; static clear(): void; static get(name: string): string | undefined; diff --git a/src/sql-synonym.js b/src/sql-synonym.js index d6fcd0b..e4d9567 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -60,6 +60,9 @@ class SqlSynonym extends Map { } static getInstance() { + if (this.instance == null) { + this.instance = new SqlSynonym(); + } return this.instance; } @@ -84,8 +87,6 @@ class SqlSynonym extends Map { } } -SqlSynonym.instance = new SqlSynonym(); - SqlFormatter.resolvingName.subscribe((event) => { if (typeof event.name !== 'string') { return; From c1ccfa89addac5b3f96021fbfd98ac0c34b9550b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:53:05 +0000 Subject: [PATCH 07/14] perf: cache sorted synonym keys and tighten singleton/type signatures Agent-Logs-Url: https://github.com/themost-framework/query/sessions/b7f59e89-589e-4a24-89b7-34005331d980 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- src/sql-synonym.d.ts | 4 ++-- src/sql-synonym.js | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts index 7142e34..fe4c3aa 100644 --- a/src/sql-synonym.d.ts +++ b/src/sql-synonym.d.ts @@ -1,8 +1,8 @@ export declare class SqlSynonym extends Map { - add(name: string | [string, string][], synonym?: string): SqlSynonym; + add(...args: [name: string, synonym: string] | [entries: [string, string][]]): SqlSynonym; resolve(name: string): string; static getInstance(): SqlSynonym; - static add(name: string | [string, string][], synonym?: string): SqlSynonym; + static add(...args: [name: string, synonym: string] | [entries: [string, string][]]): SqlSynonym; static remove(name: string): boolean; static clear(): void; static get(name: string): string | undefined; diff --git a/src/sql-synonym.js b/src/sql-synonym.js index e4d9567..4ca1a01 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -1,6 +1,11 @@ import {SqlFormatter} from './formatter'; class SqlSynonym extends Map { + constructor() { + super(); + this.sortedKeys = null; + } + add(name, synonym) { if (Array.isArray(name)) { for (const item of name) { @@ -28,6 +33,7 @@ class SqlSynonym extends Map { if (typeof synonym !== 'string') { throw new TypeError('Invalid synonym name. Expected string.'); } + this.sortedKeys = null; return super.set(name, synonym); } @@ -42,15 +48,21 @@ class SqlSynonym extends Map { if (typeof name !== 'string') { throw new TypeError('Invalid object name. Expected string.'); } + this.sortedKeys = null; return super.delete(name); } + clear() { + this.sortedKeys = null; + return super.clear(); + } + resolve(name) { const exactMatch = this.get(name); if (typeof exactMatch === 'string') { return exactMatch; } - const keys = Array.from(this.keys()).sort((a, b) => b.length - a.length); + const keys = this.sortedKeys || (this.sortedKeys = Array.from(this.keys()).sort((a, b) => b.length - a.length)); for (const key of keys) { if (name.startsWith(key + '.')) { return this.get(key).concat(name.substring(key.length)); @@ -60,7 +72,7 @@ class SqlSynonym extends Map { } static getInstance() { - if (this.instance == null) { + if (this.instance === undefined) { this.instance = new SqlSynonym(); } return this.instance; From e6e67a110b7f931b0fe7cd030bf1177bfe2c1af0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:54:12 +0000 Subject: [PATCH 08/14] refactor: align SqlSynonym map semantics and resolve validation Agent-Logs-Url: https://github.com/themost-framework/query/sessions/b7f59e89-589e-4a24-89b7-34005331d980 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- src/sql-synonym.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/sql-synonym.js b/src/sql-synonym.js index 4ca1a01..d10ba95 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -38,9 +38,6 @@ class SqlSynonym extends Map { } get(name) { - if (typeof name !== 'string') { - throw new TypeError('Invalid object name. Expected string.'); - } return super.get(name); } @@ -58,11 +55,14 @@ class SqlSynonym extends Map { } 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; } - const keys = this.sortedKeys || (this.sortedKeys = Array.from(this.keys()).sort((a, b) => b.length - a.length)); + const keys = this.getSortedKeys(); for (const key of keys) { if (name.startsWith(key + '.')) { return this.get(key).concat(name.substring(key.length)); @@ -71,6 +71,13 @@ class SqlSynonym extends Map { return name; } + getSortedKeys() { + if (this.sortedKeys === null) { + this.sortedKeys = Array.from(this.keys()).sort((a, b) => b.length - a.length); + } + return this.sortedKeys; + } + static getInstance() { if (this.instance === undefined) { this.instance = new SqlSynonym(); From cbfc2dce592b8c20758511834fb788a976b6433a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 20:01:30 +0000 Subject: [PATCH 09/14] refactor: remove redundant SqlSynonym static proxy methods Agent-Logs-Url: https://github.com/themost-framework/query/sessions/331f93ab-0b91-4358-9d97-4b7d7d38e439 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 18 ++++++++++-------- src/sql-synonym.d.ts | 5 ----- src/sql-synonym.js | 20 -------------------- 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js index 8b999d8..749a02d 100644 --- a/spec/SqlSynonym.spec.js +++ b/spec/SqlSynonym.spec.js @@ -1,16 +1,18 @@ import {OpenDataQueryFormatter, QueryExpression, SqlFormatter, SqlSynonym} from '../src/index'; describe('SqlSynonym', () => { + const synonyms = SqlSynonym.getInstance(); + beforeEach(() => { - SqlSynonym.clear(); + synonyms.clear(); }); afterAll(() => { - SqlSynonym.clear(); + synonyms.clear(); }); it('should format query by using data object synonym', () => { - SqlSynonym.add('ProductData', 'MyProduct'); + synonyms.add('ProductData', 'MyProduct'); const formatter = new SqlFormatter(); formatter.settings.nameFormat = '`$1`'; const query = new QueryExpression().select('id', 'name') @@ -20,7 +22,7 @@ describe('SqlSynonym', () => { }); it('should format qualified object names by using synonym', () => { - SqlSynonym.add('Production.Product', 'MyProduct'); + synonyms.add('Production.Product', 'MyProduct'); const formatter = new SqlFormatter(); formatter.settings.nameFormat = '`$1`'; expect(formatter.escapeEntity('Production.Product')).toBe('`MyProduct`'); @@ -28,17 +30,17 @@ describe('SqlSynonym', () => { }); it('should apply synonyms in formatter subclasses', () => { - SqlSynonym.add('Products', 'MyProducts'); + synonyms.add('Products', 'MyProducts'); const formatter = new OpenDataQueryFormatter(); expect(formatter.escapeName('Products.id')).toBe('MyProducts/id'); }); it('should add synonyms from array entries', () => { - SqlSynonym.add([ + synonyms.add([ ['object1', 'synonym1'], ['object2', 'synonym2'] ]); - expect(SqlSynonym.get('object1')).toBe('synonym1'); - expect(SqlSynonym.get('object2')).toBe('synonym2'); + expect(synonyms.get('object1')).toBe('synonym1'); + expect(synonyms.get('object2')).toBe('synonym2'); }); }); diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts index fe4c3aa..21c2286 100644 --- a/src/sql-synonym.d.ts +++ b/src/sql-synonym.d.ts @@ -2,9 +2,4 @@ export declare class SqlSynonym extends Map { add(...args: [name: string, synonym: string] | [entries: [string, string][]]): SqlSynonym; resolve(name: string): string; static getInstance(): SqlSynonym; - static add(...args: [name: string, synonym: string] | [entries: [string, string][]]): SqlSynonym; - static remove(name: string): boolean; - static clear(): void; - static get(name: string): string | undefined; - static resolve(name: string): string; } diff --git a/src/sql-synonym.js b/src/sql-synonym.js index d10ba95..17239e6 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -84,26 +84,6 @@ class SqlSynonym extends Map { } return this.instance; } - - static add(name, synonym) { - return this.getInstance().add(name, synonym); - } - - static remove(name) { - return this.getInstance().delete(name); - } - - static clear() { - this.getInstance().clear(); - } - - static get(name) { - return this.getInstance().get(name); - } - - static resolve(name) { - return this.getInstance().resolve(name); - } } SqlFormatter.resolvingName.subscribe((event) => { From 9f308a35d8398eb751964a7e2c450d7df8790589 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 04:34:35 +0000 Subject: [PATCH 10/14] refactor: simplify SqlSynonym map behavior Agent-Logs-Url: https://github.com/themost-framework/query/sessions/ab666de7-8718-440b-bcf8-2a225953ac03 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 14 ++++++------- src/sql-synonym.d.ts | 3 ++- src/sql-synonym.js | 46 +++++++++-------------------------------- 3 files changed, 19 insertions(+), 44 deletions(-) diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js index 749a02d..1a1266f 100644 --- a/spec/SqlSynonym.spec.js +++ b/spec/SqlSynonym.spec.js @@ -12,7 +12,7 @@ describe('SqlSynonym', () => { }); it('should format query by using data object synonym', () => { - synonyms.add('ProductData', 'MyProduct'); + synonyms.set('ProductData', 'MyProduct'); const formatter = new SqlFormatter(); formatter.settings.nameFormat = '`$1`'; const query = new QueryExpression().select('id', 'name') @@ -22,7 +22,7 @@ describe('SqlSynonym', () => { }); it('should format qualified object names by using synonym', () => { - synonyms.add('Production.Product', 'MyProduct'); + synonyms.set('Production.Product', 'MyProduct'); const formatter = new SqlFormatter(); formatter.settings.nameFormat = '`$1`'; expect(formatter.escapeEntity('Production.Product')).toBe('`MyProduct`'); @@ -30,17 +30,17 @@ describe('SqlSynonym', () => { }); it('should apply synonyms in formatter subclasses', () => { - synonyms.add('Products', 'MyProducts'); + synonyms.set('Products', 'MyProducts'); const formatter = new OpenDataQueryFormatter(); expect(formatter.escapeName('Products.id')).toBe('MyProducts/id'); }); - it('should add synonyms from array entries', () => { - synonyms.add([ + it('should construct synonyms from array entries', () => { + const localSynonyms = new SqlSynonym([ ['object1', 'synonym1'], ['object2', 'synonym2'] ]); - expect(synonyms.get('object1')).toBe('synonym1'); - expect(synonyms.get('object2')).toBe('synonym2'); + expect(localSynonyms.get('object1')).toBe('synonym1'); + expect(localSynonyms.get('object2')).toBe('synonym2'); }); }); diff --git a/src/sql-synonym.d.ts b/src/sql-synonym.d.ts index 21c2286..597f051 100644 --- a/src/sql-synonym.d.ts +++ b/src/sql-synonym.d.ts @@ -1,5 +1,6 @@ export declare class SqlSynonym extends Map { - add(...args: [name: string, synonym: string] | [entries: [string, string][]]): SqlSynonym; + constructor(); + constructor(entries?: readonly (readonly [string, string])[] | null); resolve(name: string): string; static getInstance(): SqlSynonym; } diff --git a/src/sql-synonym.js b/src/sql-synonym.js index 17239e6..329e014 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -1,29 +1,8 @@ import {SqlFormatter} from './formatter'; class SqlSynonym extends Map { - constructor() { - super(); - this.sortedKeys = null; - } - - add(name, synonym) { - if (Array.isArray(name)) { - for (const item of name) { - if (!Array.isArray(item) || item.length !== 2) { - throw new TypeError('Invalid synonym entry. Expected [name, synonym].'); - } - this.set(item[0], item[1]); - } - return this; - } - if (typeof name !== 'string') { - throw new TypeError('Invalid object name. Expected string.'); - } - if (typeof synonym !== 'string') { - throw new TypeError('Invalid synonym name. Expected string.'); - } - this.set(name, synonym); - return this; + constructor(entries) { + super(entries); } set(name, synonym) { @@ -33,7 +12,6 @@ class SqlSynonym extends Map { if (typeof synonym !== 'string') { throw new TypeError('Invalid synonym name. Expected string.'); } - this.sortedKeys = null; return super.set(name, synonym); } @@ -45,12 +23,10 @@ class SqlSynonym extends Map { if (typeof name !== 'string') { throw new TypeError('Invalid object name. Expected string.'); } - this.sortedKeys = null; return super.delete(name); } clear() { - this.sortedKeys = null; return super.clear(); } @@ -62,20 +38,18 @@ class SqlSynonym extends Map { if (typeof exactMatch === 'string') { return exactMatch; } - const keys = this.getSortedKeys(); - for (const key of keys) { + let candidate; + for (const key of this.keys()) { if (name.startsWith(key + '.')) { - return this.get(key).concat(name.substring(key.length)); + if (candidate === undefined || key.length > candidate.length) { + candidate = key; + } } } - return name; - } - - getSortedKeys() { - if (this.sortedKeys === null) { - this.sortedKeys = Array.from(this.keys()).sort((a, b) => b.length - a.length); + if (typeof candidate === 'string') { + return this.get(candidate).concat(name.substring(candidate.length)); } - return this.sortedKeys; + return name; } static getInstance() { From 915f1e70f9036ff7420207cc72385558864280be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 05:45:02 +0000 Subject: [PATCH 11/14] refactor: reverse SqlSynonym mapping to SQL synonym direction Agent-Logs-Url: https://github.com/themost-framework/query/sessions/0af5db8f-0c46-4675-bd4a-edfb48695bff Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 26 +++++++++++++------------- src/sql-synonym.js | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js index 1a1266f..1bb98d0 100644 --- a/spec/SqlSynonym.spec.js +++ b/spec/SqlSynonym.spec.js @@ -11,36 +11,36 @@ describe('SqlSynonym', () => { synonyms.clear(); }); - it('should format query by using data object synonym', () => { - synonyms.set('ProductData', 'MyProduct'); + 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('ProductData').where('id').equal(100); + .from('Products').where('id').equal(100); const sql = formatter.formatSelect(query); - expect(sql).toBe('SELECT `MyProduct`.`id`, `MyProduct`.`name` FROM `MyProduct` WHERE (`id`=100)'); + expect(sql).toBe('SELECT `sales`.`Products`.`id`, `sales`.`Products`.`name` FROM `sales`.`Products` WHERE (`id`=100)'); }); it('should format qualified object names by using synonym', () => { - synonyms.set('Production.Product', 'MyProduct'); + synonyms.set('Products', 'Production.Product'); const formatter = new SqlFormatter(); formatter.settings.nameFormat = '`$1`'; - expect(formatter.escapeEntity('Production.Product')).toBe('`MyProduct`'); - expect(formatter.escapeName('Production.Product.ProductID')).toBe('`MyProduct`.`ProductID`'); + 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', 'MyProducts'); + synonyms.set('Products', 'sales.Products'); const formatter = new OpenDataQueryFormatter(); - expect(formatter.escapeName('Products.id')).toBe('MyProducts/id'); + expect(formatter.escapeName('Products.id')).toBe('sales/Products/id'); }); it('should construct synonyms from array entries', () => { const localSynonyms = new SqlSynonym([ - ['object1', 'synonym1'], - ['object2', 'synonym2'] + ['synonym1', 'schema1.object1'], + ['synonym2', 'schema2.object2'] ]); - expect(localSynonyms.get('object1')).toBe('synonym1'); - expect(localSynonyms.get('object2')).toBe('synonym2'); + expect(localSynonyms.get('synonym1')).toBe('schema1.object1'); + expect(localSynonyms.get('synonym2')).toBe('schema2.object2'); }); }); diff --git a/src/sql-synonym.js b/src/sql-synonym.js index 329e014..f49e4ce 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -5,25 +5,25 @@ class SqlSynonym extends Map { super(entries); } - set(name, synonym) { - if (typeof name !== 'string') { - throw new TypeError('Invalid object name. Expected string.'); - } + set(synonym, name) { if (typeof synonym !== 'string') { throw new TypeError('Invalid synonym name. Expected string.'); } - return super.set(name, synonym); + if (typeof name !== 'string') { + throw new TypeError('Invalid object name. Expected string.'); + } + return super.set(synonym, name); } - get(name) { - return super.get(name); + get(synonym) { + return super.get(synonym); } - delete(name) { - if (typeof name !== 'string') { - throw new TypeError('Invalid object name. Expected string.'); + delete(synonym) { + if (typeof synonym !== 'string') { + throw new TypeError('Invalid synonym name. Expected string.'); } - return super.delete(name); + return super.delete(synonym); } clear() { From 1f63850e1d193f64be2cbf64621f8dd179f66160 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 05:46:08 +0000 Subject: [PATCH 12/14] chore: align SqlSynonym naming and test readability Agent-Logs-Url: https://github.com/themost-framework/query/sessions/0af5db8f-0c46-4675-bd4a-edfb48695bff Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 3 ++- src/sql-synonym.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js index 1bb98d0..789c15c 100644 --- a/spec/SqlSynonym.spec.js +++ b/spec/SqlSynonym.spec.js @@ -18,7 +18,8 @@ describe('SqlSynonym', () => { const query = new QueryExpression().select('id', 'name') .from('Products').where('id').equal(100); const sql = formatter.formatSelect(query); - expect(sql).toBe('SELECT `sales`.`Products`.`id`, `sales`.`Products`.`name` FROM `sales`.`Products` WHERE (`id`=100)'); + 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', () => { diff --git a/src/sql-synonym.js b/src/sql-synonym.js index f49e4ce..b370540 100644 --- a/src/sql-synonym.js +++ b/src/sql-synonym.js @@ -10,7 +10,7 @@ class SqlSynonym extends Map { throw new TypeError('Invalid synonym name. Expected string.'); } if (typeof name !== 'string') { - throw new TypeError('Invalid object name. Expected string.'); + throw new TypeError('Invalid target name. Expected string.'); } return super.set(synonym, name); } From 176ef4ed3177ea5d2e0fb2bd5e035cff67e84b2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 06:19:34 +0000 Subject: [PATCH 13/14] feat: support formatter-level synonym instance resolution Agent-Logs-Url: https://github.com/themost-framework/query/sessions/4dd5caf9-c9a3-468d-a793-26caab8368b4 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- spec/SqlSynonym.spec.js | 10 ++++++++++ src/formatter.d.ts | 1 + src/formatter.js | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/spec/SqlSynonym.spec.js b/spec/SqlSynonym.spec.js index 789c15c..b4a0e28 100644 --- a/spec/SqlSynonym.spec.js +++ b/spec/SqlSynonym.spec.js @@ -36,6 +36,16 @@ describe('SqlSynonym', () => { 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'], diff --git a/src/formatter.d.ts b/src/formatter.d.ts index f4dd98a..5d2b0a6 100644 --- a/src/formatter.d.ts +++ b/src/formatter.d.ts @@ -13,6 +13,7 @@ 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; diff --git a/src/formatter.js b/src/formatter.js index 40e0f2d..0a8b6f2 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -46,6 +46,7 @@ class SqlFormatter { constructor() { // this.provider = null; + this.synonyms = null; this.resolvingName = SqlFormatter.resolvingName; /** * Gets or sets formatter settings @@ -1099,6 +1100,9 @@ class SqlFormatter { if (isNameReference(str)) { str = trimNameReference(name); } + if (this.synonyms && typeof this.synonyms.resolve === 'function') { + str = this.synonyms.resolve(str); + } const event = { target: this, name: str @@ -1115,6 +1119,9 @@ class SqlFormatter { if (isNameReference(str)) { str = trimNameReference(name); } + if (this.synonyms && typeof this.synonyms.resolve === 'function') { + str = this.synonyms.resolve(str); + } const event = { target: this, name: str From ecf410fa4f96b0c2b24158c7efb926b56e3ef173 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 06:21:00 +0000 Subject: [PATCH 14/14] refactor: deduplicate formatter name resolution logic Agent-Logs-Url: https://github.com/themost-framework/query/sessions/4dd5caf9-c9a3-468d-a793-26caab8368b4 Co-authored-by: kbarbounakis <9191768+kbarbounakis@users.noreply.github.com> --- src/formatter.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/formatter.js b/src/formatter.js index 0a8b6f2..aecc8f0 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -37,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 @@ -1096,16 +1107,9 @@ class SqlFormatter { if (typeof name !== 'string') { throw new Error('Invalid name expression. Expected string.'); } - let str = name; - if (isNameReference(str)) { - str = trimNameReference(name); - } - if (this.synonyms && typeof this.synonyms.resolve === 'function') { - str = this.synonyms.resolve(str); - } const event = { target: this, - name: str + name: resolveName(this, name) }; this.resolvingName.emit(event); return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat); @@ -1115,16 +1119,9 @@ class SqlFormatter { if (typeof name !== 'string') { throw new Error('Invalid entity expression. Expected string.'); } - let str = name; - if (isNameReference(str)) { - str = trimNameReference(name); - } - if (this.synonyms && typeof this.synonyms.resolve === 'function') { - str = this.synonyms.resolve(str); - } const event = { target: this, - name: str + name: resolveName(this, name) }; this.resolvingName.emit(event); return ObjectNameValidator.validator.escape(event.name, this.settings.nameFormat);