feat(skd-edit): patch-query @once — assert ровно одно вхождение

Защищает от случайных замен в комментариях/совпадениях имён:

  "ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ @once"
  # fail, если в запросе 0 или 2+ вхождений

Без флага default — replace-all (как раньше, обратная совместимость).

При успехе сообщение содержит фактическое число вхождений
"(N occurrence(s))", помогает заметить неожиданную множественность
без явного @once.

Регресс: 31/31 PS, 31/31 PY, 31/31 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-15 16:00:55 +03:00
parent e7cbf306a0
commit f0f1e88aaa
6 changed files with 123 additions and 8 deletions
+4 -2
View File
@@ -253,13 +253,15 @@ Value — имена ресурсов (как в полях/вычисляемы
### patch-query — точечная замена в тексте запроса
Shorthand: `"старое => новое"`. Заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
```
"СубконтоДт1) В => СубконтоКт1) В"
"ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ"
"ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ @once"
```
`@once` — упасть с ошибкой, если в запросе не **ровно одно** вхождение. Защищает от случайных замен в комментариях и однотипных идентификаторах.
### set-outputParameter — установить параметр вывода
```
+17 -3
View File
@@ -1,4 +1,4 @@
# skd-edit v1.15 — Atomic 1C DCS editor
# skd-edit v1.16 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -2472,6 +2472,12 @@ switch ($Operation) {
}
foreach ($val in $values) {
$once = $false
if ($val -match '@once\b') {
$once = $true
$val = ($val -replace '\s*@once\b', '').Trim()
}
$sepIdx = $val.IndexOf(" => ")
if ($sepIdx -lt 0) {
Write-Error "patch-query value must contain ' => ' separator: old => new"
@@ -2480,12 +2486,20 @@ switch ($Operation) {
$oldStr = $val.Substring(0, $sepIdx)
$newStr = $val.Substring($sepIdx + 4)
$queryText = $queryEl.InnerText
if (-not $queryText.Contains($oldStr)) {
$count = ([regex]::Matches($queryText, [regex]::Escape($oldStr))).Count
if ($count -eq 0) {
Write-Error "Substring not found in query of dataset '$dsName': $oldStr"
exit 1
}
if ($once -and $count -ne 1) {
Write-Error "@once: expected 1 occurrence of '$oldStr' in dataset '$dsName', found $count"
exit 1
}
$queryEl.InnerText = $queryText.Replace($oldStr, $newStr)
Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'"
$suffix = if ($once) { " (1 occurrence)" } else { " ($count occurrence(s))" }
Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix"
}
}
+15 -3
View File
@@ -1,4 +1,4 @@
# skd-edit v1.15 — Atomic 1C DCS editor (Python port)
# skd-edit v1.16 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -2064,6 +2064,11 @@ elif operation == "patch-query":
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
sys.exit(1)
for val in values:
once = False
if re.search(r'@once\b', val):
once = True
val = re.sub(r'\s*@once\b', '', val).strip()
sep_idx = val.find(" => ")
if sep_idx < 0:
print("patch-query value must contain ' => ' separator: old => new", file=sys.stderr)
@@ -2071,11 +2076,18 @@ elif operation == "patch-query":
old_str = val[:sep_idx]
new_str = val[sep_idx + 4:]
query_text = query_el.text or ""
if old_str not in query_text:
count = query_text.count(old_str)
if count == 0:
print(f"Substring not found in query of dataset '{ds_name}': {old_str}", file=sys.stderr)
sys.exit(1)
if once and count != 1:
print(f"@once: expected 1 occurrence of '{old_str}' in dataset '{ds_name}', found {count}", file=sys.stderr)
sys.exit(1)
query_el.text = query_text.replace(old_str, new_str)
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'')
suffix = " (1 occurrence)" if once else f" ({count} occurrence(s))"
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'{suffix}')
elif operation == "set-outputParameter":
settings = resolve_variant_settings()
@@ -0,0 +1,22 @@
{
"name": "patch-query @once: множественные вхождения — ошибка",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.Поле1, Т.Поле2 ИЗ Регистр КАК Т ГДЕ Т.Поле1 = Т.Поле2",
"fields": ["Поле1: string", "Поле2: string"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": {
"templatePath": "Template.xml",
"operation": "patch-query",
"value": "Поле1 => Новое @once"
},
"expectError": "expected 1 occurrence"
}
@@ -0,0 +1,21 @@
{
"name": "patch-query @once: 1 вхождение — успешная замена",
"preRun": [
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Основной",
"query": "ВЫБРАТЬ Т.УникальноеИмя ИЗ Регистр КАК Т",
"fields": ["УникальноеИмя: string"]
}]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "{workDir}/Template.xml" }
}
],
"params": {
"templatePath": "Template.xml",
"operation": "patch-query",
"value": "УникальноеИмя => НовоеИмя @once"
}
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Основной</name>
<field xsi:type="DataSetFieldField">
<dataPath>УникальноеИмя</dataPath>
<field>УникальноеИмя</field>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ Т.НовоеИмя ИЗ Регистр КАК Т</query>
</dataSet>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>