Skip to content

Commit 45b6616

Browse files
committed
Avoid regex template rendering in SQL source
1 parent 72dff0a commit 45b6616

2 files changed

Lines changed: 66 additions & 3 deletions

File tree

packages/agent/src/generic-sql-source.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,12 +752,43 @@ function parseQualifiedField(value: string): { dataset?: string; field: string }
752752
}
753753

754754
function renderTemplate(template: string, row: GenericSqlRow): string {
755-
return template.replace(/\{([^}]+)\}/g, (_, field: string) => stringifyRawValue(row[field]));
755+
const rendered: string[] = [];
756+
let cursor = 0;
757+
while (cursor < template.length) {
758+
const open = template.indexOf('{', cursor);
759+
if (open === -1) {
760+
rendered.push(template.slice(cursor));
761+
break;
762+
}
763+
const close = template.indexOf('}', open + 1);
764+
if (close === -1) {
765+
rendered.push(template.slice(cursor));
766+
break;
767+
}
768+
769+
rendered.push(template.slice(cursor, open));
770+
const field = template.slice(open + 1, close);
771+
rendered.push(field ? stringifyRawValue(row[field]) : template.slice(open, close + 1));
772+
cursor = close + 1;
773+
}
774+
return rendered.join('');
756775
}
757776

758777
function resolveTemplateArgument(value: string, row: GenericSqlRow): unknown {
759-
const exact = /^\{([^}]+)\}$/.exec(value);
760-
return exact ? row[exact[1]!] : renderTemplate(value, row);
778+
const exact = exactTemplateField(value);
779+
return exact ? row[exact] : renderTemplate(value, row);
780+
}
781+
782+
function exactTemplateField(value: string): string | null {
783+
if (!value.startsWith('{') || !value.endsWith('}') || value.length <= 2) {
784+
return null;
785+
}
786+
const close = value.indexOf('}', 1);
787+
if (close !== value.length - 1) {
788+
return null;
789+
}
790+
const field = value.slice(1, -1);
791+
return field || null;
761792
}
762793

763794
function stringifyRawValue(value: unknown): string {

packages/agent/test/generic-sql-source.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,38 @@ describe('generic SQL source handler', () => {
7878
}))).rejects.toThrow('dataset orders is missing required columns: quantity');
7979
});
8080

81+
it('renders mapping templates without regular expression replacement', async () => {
82+
registerGenericSqlConnector(TEST_DIALECT, async (): Promise<GenericSqlClient> => ({
83+
async query(_sql, _parameters, dataset) {
84+
return rowsByDataset()[dataset] ?? [];
85+
},
86+
}));
87+
88+
const result = await genericSqlSourceHandler.prepare(memorySource({
89+
...mapping,
90+
entities: [
91+
{
92+
name: 'order',
93+
from: 'orders',
94+
id: 'urn:neutral:order:{order_id}',
95+
properties: {
96+
'https://example.test/composite': 'order-{order_id}-{status}',
97+
'https://example.test/emptyBraces': 'literal-{}',
98+
'https://example.test/unclosedBrace': 'literal-{status',
99+
},
100+
},
101+
],
102+
relations: [],
103+
}));
104+
105+
const quads = result.assets.flatMap((asset) => asset.quads);
106+
expect(quads).toEqual(expect.arrayContaining([
107+
quad('urn:neutral:order:A100', 'https://example.test/composite', '"order-A100-ready"'),
108+
quad('urn:neutral:order:A100', 'https://example.test/emptyBraces', '"literal-{}"'),
109+
quad('urn:neutral:order:A100', 'https://example.test/unclosedBrace', '"literal-{status"'),
110+
]));
111+
});
112+
81113
it('selects only asset groups for changed partitions', async () => {
82114
let rows = rowsByDataset();
83115
registerGenericSqlConnector(TEST_DIALECT, async (): Promise<GenericSqlClient> => ({

0 commit comments

Comments
 (0)