kcode-pi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +358 -0
- package/dist/cli/kcode.d.ts +15 -0
- package/dist/cli/kcode.js +153 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +7 -0
- package/docs/KCODE_DISTRIBUTION.md +91 -0
- package/extensions/kingdee-harness.ts +180 -0
- package/extensions/kingdee-header.ts +122 -0
- package/extensions/kingdee-tools.ts +379 -0
- package/knowledge/.backup/v1.0.0/version.json +10 -0
- package/knowledge/cangqiong/product-notes.md +15 -0
- package/knowledge/common/business-flows.md +115 -0
- package/knowledge/common/config-guides.md +110 -0
- package/knowledge/common/error-patterns.md +170 -0
- package/knowledge/common/implementation.md +144 -0
- package/knowledge/cosmic/hard-constraints.md +38 -0
- package/knowledge/cosmic/ksql-datafix.md +34 -0
- package/knowledge/cosmic/platform-baseline.md +32 -0
- package/knowledge/cosmic/plugin-decision-matrix.md +40 -0
- package/knowledge/cosmic/review-checklist.md +40 -0
- package/knowledge/cosmic/unittest.md +35 -0
- package/knowledge/enterprise/api-reference.md +186 -0
- package/knowledge/enterprise/code-patterns.md +217 -0
- package/knowledge/enterprise/plugin-lifecycle.md +188 -0
- package/knowledge/enterprise/tables.json +159 -0
- package/knowledge/flagship/api-reference.md +237 -0
- package/knowledge/flagship/code-patterns.md +246 -0
- package/knowledge/flagship/cosmic-platform-note.md +15 -0
- package/knowledge/flagship/plugin-lifecycle.md +248 -0
- package/knowledge/flagship/tables.json +159 -0
- package/knowledge/version.json +10 -0
- package/knowledge/xinghan/product-notes.md +15 -0
- package/package.json +71 -0
- package/prompts/kd-discuss.md +11 -0
- package/prompts/kd-execute.md +12 -0
- package/prompts/kd-plan.md +12 -0
- package/prompts/kd-ship.md +12 -0
- package/prompts/kd-spec.md +12 -0
- package/prompts/kd-verify.md +12 -0
- package/skills/kd-check/SKILL.md +26 -0
- package/skills/kd-cosmic-dev/SKILL.md +82 -0
- package/skills/kd-cosmic-review/SKILL.md +90 -0
- package/skills/kd-cosmic-unittest/SKILL.md +92 -0
- package/skills/kd-debug/SKILL.md +30 -0
- package/skills/kd-discuss/SKILL.md +24 -0
- package/skills/kd-execute/SKILL.md +22 -0
- package/skills/kd-gen/SKILL.md +34 -0
- package/skills/kd-ksql/SKILL.md +86 -0
- package/skills/kd-plan/SKILL.md +24 -0
- package/skills/kd-ship/SKILL.md +22 -0
- package/skills/kd-spec/SKILL.md +24 -0
- package/skills/kd-verify/SKILL.md +22 -0
- package/themes/kcode-dark.json +81 -0
- package/vendor/kingdee-skills/cosmic-unittest/SKILL.md +788 -0
- package/vendor/kingdee-skills/cosmic-unittest/author-cache.json +5 -0
- package/vendor/kingdee-skills/cosmic-unittest/cosmic-unittest-skill-overview.html +746 -0
- package/vendor/kingdee-skills/cosmic-unittest/examples/business-test.md +205 -0
- package/vendor/kingdee-skills/cosmic-unittest/examples/common-test.md +257 -0
- package/vendor/kingdee-skills/cosmic-unittest/examples/formplugin-test.md +560 -0
- package/vendor/kingdee-skills/cosmic-unittest/examples/op-plugin-test.md +231 -0
- package/vendor/kingdee-skills/cosmic-unittest/examples/validator-test.md +232 -0
- package/vendor/kingdee-skills/cosmic-unittest/patterns/business-helper.md +184 -0
- package/vendor/kingdee-skills/cosmic-unittest/patterns/common-module.md +355 -0
- package/vendor/kingdee-skills/cosmic-unittest/patterns/convert-plugin.md +130 -0
- package/vendor/kingdee-skills/cosmic-unittest/patterns/formplugin.md +235 -0
- package/vendor/kingdee-skills/cosmic-unittest/patterns/op-plugin.md +226 -0
- package/vendor/kingdee-skills/cosmic-unittest/patterns/validator.md +206 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/SKILL.md +674 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/advanced-scenario-checklist.md +307 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/algox-performance-checklist.md +129 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/coding-standard-checklist.md +491 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/cosmic-api-checklist.md +285 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/data-access-checklist.md +261 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/data-transaction-checklist.md +390 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/domain-logic-checklist.md +295 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/form-plugin-checklist.md +508 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/infra-checklist.md +254 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/ksql-checklist.md +305 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/lifecycle-checklist.md +298 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/operation-plugin-checklist.md +442 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/test-mock-checklist.md +120 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/references/ui-performance-checklist.md +320 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/pattern-matcher.py +336 -0
- package/vendor/kingdee-skills/kingdee-cosmic-reviewer/scripts/review-score-calculator.py +121 -0
- package/vendor/kingdee-skills/ok-cosmic/CHANGELOG.md +295 -0
- package/vendor/kingdee-skills/ok-cosmic/README.md +460 -0
- package/vendor/kingdee-skills/ok-cosmic/SKILL.md +287 -0
- package/vendor/kingdee-skills/ok-cosmic/agents/openai.yaml +17 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/BatchImportPluginTemplate.java +93 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/BillPlugInTemplate.java +156 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/ConvertPlugInTemplate.java +255 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/FormPluginTemplate.java +597 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/IWorkflowPluginTemplate.java +91 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/ListPluginTemplate.java +194 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/OpPluginTemplate.java +201 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/OpenApiControllerTemplate.java +103 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/PrintPluginTemplate.java +95 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/ReportFormPluginTemplate.java +257 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/ReportListDataPluginTemplate.java +70 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/StandardTreeListPluginTemplate.java +130 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/TaskTemplate.java +80 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/TreeListPluginTemplate.java +152 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/WriteBackPlugInTemplate.java +286 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/attachment/AttachmentUploadBindSample.java +93 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/botp/BotpTracePushSample.java +168 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/botp/SampleConvertPlugin.java +223 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/cache/SampleCacheUsage.java +218 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/concurrent/SampleThreadPoolBatch.java +156 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/data/DynamicObjectCrudSample.java +205 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/data/DynamicObjectOpsSample.java +100 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/BeforeOperationConfirmSample.java +217 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/ConfirmDialogSample.java +131 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/EntryRowCalculateSample.java +116 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/F7FilterSample.java +134 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/GetAndSetValueSample.java +176 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/HyperlinkJumpSample.java +124 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/OpenBillModalSample.java +253 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/ReturnParentDataSample.java +295 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/TreeControlSample.java +140 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/form/ViewControlOpsSample.java +132 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/list/ListPluginBasicSample.java +170 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/list/ListPreOpenFilterSample.java +68 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/message/MessageNotifySample.java +95 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/mq/SampleMQConsumer.java +198 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/mq/sample_mq.xml +15 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/operation/OpAddValidatorsSample.java +137 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/operation/OperationOptionBridgeSample.java +228 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/package-info.java +19 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/query/BaseDataQuerySample.java +194 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/query/BatchQuerySample.java +368 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/query/DataSetQueryStatSample.java +131 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/report/SampleReportFormPlugin.java +179 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/report/SampleReportListDataPlugin.java +616 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/snippets-guide.md +64 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/task/ScheduleTaskSample.java +160 -0
- package/vendor/kingdee-skills/ok-cosmic/assets/snippets/workflow/SampleWorkflowPlugin.java +302 -0
- package/vendor/kingdee-skills/ok-cosmic/manifest.json +78 -0
- package/vendor/kingdee-skills/ok-cosmic/ok-cosmic-intro.html +903 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/attachment-api.md +114 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/botp-convert.md +98 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/dynamic-object.md +113 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/entity-metadata.md +123 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/event-lifecycle.md +184 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/flex-prop.md +114 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/form-utils.md +133 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/operate-chain.md +159 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/plugin-base.md +218 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/query-dataset.md +149 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/request-context.md +88 -0
- package/vendor/kingdee-skills/ok-cosmic/references/adv/view-handler.md +157 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-bill.md +76 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-botp.md +70 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-form.md +165 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-import.md +69 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-list.md +227 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-openapi.md +112 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-operation.md +135 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-print.md +65 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-report-data.md +64 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-report-form.md +90 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-task.md +62 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-tree-list.md +71 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-workflow.md +82 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/plugin/plugin-writeback.md +71 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-algo.md +67 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-cache.md +63 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-dynamic-model-svc.md +82 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-dynamic-object.md +70 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-entity-model.md +61 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-exception.md +64 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-file.md +63 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-id.md +47 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-lock.md +61 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-log.md +63 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-network-control.md +70 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-orm-access.md +78 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-request-context.md +62 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-threadpool.md +63 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-tx.md +64 -0
- package/vendor/kingdee-skills/ok-cosmic/references/base/sdk/sdk-utils.md +67 -0
- package/vendor/kingdee-skills/ok-cosmic/requirements.txt +2 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/a-layer-rules.json +24 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/anti-patterns.md +48 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/cheat-sheet.md +256 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/coding-preferences.md +140 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/constraints.md +61 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/decision-matrix.md +222 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/intent-routing.md +94 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/platform-baseline.md +69 -0
- package/vendor/kingdee-skills/ok-cosmic/rules/post-check.md +109 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/config_loader.py +204 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-api-knowledge.py +910 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-basedata-query.py +359 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-config-check.py +181 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-extpoints-query.py +389 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-form-metadata.py +856 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-check.py +262 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/cosmic-post-lint.py +293 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/__init__.py +2 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/base.py +393 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/resource_check.py +176 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/scene_check.py +375 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/style_check.py +434 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/lint/verify_check.py +36 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/route_client.py +186 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/script_utils.py +40 -0
- package/vendor/kingdee-skills/ok-cosmic/scripts/sqlite_cache.py +142 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-commons.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/cuslib/kd-cd-cosmic-features.jar +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/ok-cosmic-docs.db +0 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/ok-cosmic.json +13 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-mac.sh +18 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup-windows.bat +53 -0
- package/vendor/kingdee-skills/ok-cosmic/setup/setup.jar +0 -0
- package/vendor/kingdee-skills/ok-ksql/SKILL.md +81 -0
- package/vendor/kingdee-skills/ok-ksql/agents/openai.yaml +7 -0
- package/vendor/kingdee-skills/ok-ksql/manifest.json +14 -0
- package/vendor/kingdee-skills/ok-ksql/references/ksql-datafix.md +452 -0
- package/vendor/kingdee-skills/ok-ksql/scripts/ksql_lint.py +363 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Static checks for ok-ksql generated PostgreSQL data-fix SQL files.
|
|
3
|
+
|
|
4
|
+
The linter is intentionally dependency-free and conservative: it catches the
|
|
5
|
+
high-risk patterns required by the ok-ksql skill, but it is not a full SQL
|
|
6
|
+
parser. Fix reported ERROR items before delivery. WARN items document style
|
|
7
|
+
preferences that may be acceptable only with an explicit reason.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Iterable, Iterator, Sequence
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
TIMESTAMP_RE = re.compile(r"\b\d{12}\b")
|
|
21
|
+
BACKUP_TABLE_RE = re.compile(r"\bbak_[a-zA-Z0-9_]+_(\d{12})\b", re.IGNORECASE)
|
|
22
|
+
FILE_TS_RE = re.compile(r"ksql_[^/\\]*_(\d{12})\.txt$", re.IGNORECASE)
|
|
23
|
+
HEADER_TS_RE = re.compile(r"备份表时间戳[::]\s*(\d{12})")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Finding:
|
|
28
|
+
severity: str
|
|
29
|
+
line: int
|
|
30
|
+
message: str
|
|
31
|
+
|
|
32
|
+
def format(self, path: Path) -> str:
|
|
33
|
+
return f"{path}:{self.line}: {self.severity}: {self.message}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Statement:
|
|
38
|
+
text: str
|
|
39
|
+
line: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def strip_comments_and_literals(sql: str) -> str:
|
|
43
|
+
"""Return SQL with comments and quoted literals replaced by spaces.
|
|
44
|
+
|
|
45
|
+
This keeps line numbers stable while avoiding false positives from words
|
|
46
|
+
appearing inside comments or strings.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
out: list[str] = []
|
|
50
|
+
i = 0
|
|
51
|
+
n = len(sql)
|
|
52
|
+
state = "normal"
|
|
53
|
+
dollar_tag = ""
|
|
54
|
+
|
|
55
|
+
while i < n:
|
|
56
|
+
ch = sql[i]
|
|
57
|
+
nxt = sql[i + 1] if i + 1 < n else ""
|
|
58
|
+
|
|
59
|
+
if state == "normal":
|
|
60
|
+
if ch == "-" and nxt == "-":
|
|
61
|
+
out.extend([" ", " "])
|
|
62
|
+
i += 2
|
|
63
|
+
state = "line_comment"
|
|
64
|
+
continue
|
|
65
|
+
if ch == "/" and nxt == "*":
|
|
66
|
+
out.extend([" ", " "])
|
|
67
|
+
i += 2
|
|
68
|
+
state = "block_comment"
|
|
69
|
+
continue
|
|
70
|
+
if ch == "'":
|
|
71
|
+
out.append(" ")
|
|
72
|
+
i += 1
|
|
73
|
+
state = "single_quote"
|
|
74
|
+
continue
|
|
75
|
+
if ch == '"':
|
|
76
|
+
out.append(" ")
|
|
77
|
+
i += 1
|
|
78
|
+
state = "double_quote"
|
|
79
|
+
continue
|
|
80
|
+
if ch == "$":
|
|
81
|
+
match = re.match(r"\$[A-Za-z_][A-Za-z0-9_]*\$|\$\$", sql[i:])
|
|
82
|
+
if match:
|
|
83
|
+
dollar_tag = match.group(0)
|
|
84
|
+
out.extend(" " * len(dollar_tag))
|
|
85
|
+
i += len(dollar_tag)
|
|
86
|
+
state = "dollar_quote"
|
|
87
|
+
continue
|
|
88
|
+
out.append(ch)
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if state == "line_comment":
|
|
93
|
+
out.append("\n" if ch == "\n" else " ")
|
|
94
|
+
i += 1
|
|
95
|
+
if ch == "\n":
|
|
96
|
+
state = "normal"
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if state == "block_comment":
|
|
100
|
+
if ch == "*" and nxt == "/":
|
|
101
|
+
out.extend([" ", " "])
|
|
102
|
+
i += 2
|
|
103
|
+
state = "normal"
|
|
104
|
+
else:
|
|
105
|
+
out.append("\n" if ch == "\n" else " ")
|
|
106
|
+
i += 1
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if state == "single_quote":
|
|
110
|
+
if ch == "'" and nxt == "'":
|
|
111
|
+
out.extend([" ", " "])
|
|
112
|
+
i += 2
|
|
113
|
+
else:
|
|
114
|
+
out.append("\n" if ch == "\n" else " ")
|
|
115
|
+
i += 1
|
|
116
|
+
if ch == "'":
|
|
117
|
+
state = "normal"
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if state == "double_quote":
|
|
121
|
+
if ch == '"' and nxt == '"':
|
|
122
|
+
out.extend([" ", " "])
|
|
123
|
+
i += 2
|
|
124
|
+
else:
|
|
125
|
+
out.append("\n" if ch == "\n" else " ")
|
|
126
|
+
i += 1
|
|
127
|
+
if ch == '"':
|
|
128
|
+
state = "normal"
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if state == "dollar_quote":
|
|
132
|
+
if sql.startswith(dollar_tag, i):
|
|
133
|
+
out.extend(" " * len(dollar_tag))
|
|
134
|
+
i += len(dollar_tag)
|
|
135
|
+
state = "normal"
|
|
136
|
+
dollar_tag = ""
|
|
137
|
+
else:
|
|
138
|
+
out.append("\n" if ch == "\n" else " ")
|
|
139
|
+
i += 1
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
return "".join(out)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def iter_statements(masked_sql: str) -> Iterator[Statement]:
|
|
146
|
+
start = 0
|
|
147
|
+
start_line = 1
|
|
148
|
+
line = 1
|
|
149
|
+
|
|
150
|
+
for idx, ch in enumerate(masked_sql):
|
|
151
|
+
if ch == ";":
|
|
152
|
+
text = masked_sql[start : idx + 1]
|
|
153
|
+
if text.strip():
|
|
154
|
+
yield Statement(text=text, line=start_line)
|
|
155
|
+
start = idx + 1
|
|
156
|
+
start_line = line
|
|
157
|
+
if ch == "\n":
|
|
158
|
+
line += 1
|
|
159
|
+
if not masked_sql[start:idx].strip():
|
|
160
|
+
start_line = line
|
|
161
|
+
|
|
162
|
+
tail = masked_sql[start:]
|
|
163
|
+
if tail.strip():
|
|
164
|
+
yield Statement(text=tail, line=start_line)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def line_of_offset(text: str, base_line: int, offset: int) -> int:
|
|
168
|
+
return base_line + text[:offset].count("\n")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def has_token(statement: str, token: str) -> bool:
|
|
172
|
+
return re.search(rf"\b{re.escape(token)}\b", statement, re.IGNORECASE) is not None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def lint_statement(stmt: Statement) -> list[Finding]:
|
|
176
|
+
findings: list[Finding] = []
|
|
177
|
+
compact = " ".join(stmt.text.split())
|
|
178
|
+
if not compact:
|
|
179
|
+
return findings
|
|
180
|
+
|
|
181
|
+
first = re.match(r"^\s*(\w+)", compact)
|
|
182
|
+
first_word = first.group(1).upper() if first else ""
|
|
183
|
+
|
|
184
|
+
if first_word in {"UPDATE", "DELETE"} and not has_token(stmt.text, "WHERE"):
|
|
185
|
+
findings.append(
|
|
186
|
+
Finding(
|
|
187
|
+
"ERROR",
|
|
188
|
+
stmt.line,
|
|
189
|
+
f"{first_word} 语句缺少 WHERE,禁止生成无范围更新/删除。",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
select_star_matches = list(re.finditer(r"\bSELECT\s+\*", stmt.text, re.IGNORECASE))
|
|
194
|
+
if select_star_matches:
|
|
195
|
+
is_backup = re.search(
|
|
196
|
+
r"\bSELECT\s+\*\s+INTO\s+bak_[a-zA-Z0-9_]+_\d{12}\s+FROM\b",
|
|
197
|
+
stmt.text,
|
|
198
|
+
re.IGNORECASE,
|
|
199
|
+
)
|
|
200
|
+
if is_backup:
|
|
201
|
+
if has_token(stmt.text, "WHERE"):
|
|
202
|
+
findings.append(
|
|
203
|
+
Finding(
|
|
204
|
+
"ERROR",
|
|
205
|
+
stmt.line,
|
|
206
|
+
"备份语句必须整表备份,SELECT * INTO bak_... 不允许带 WHERE。",
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
for match in select_star_matches:
|
|
211
|
+
findings.append(
|
|
212
|
+
Finding(
|
|
213
|
+
"ERROR",
|
|
214
|
+
line_of_offset(stmt.text, stmt.line, match.start()),
|
|
215
|
+
"查询/验证语句禁止 SELECT *;只有整表备份 SELECT * INTO bak_... 例外。",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if re.search(r"\bSELECT\s+\*\s+INTO\b", stmt.text, re.IGNORECASE) and not re.search(
|
|
220
|
+
r"\bSELECT\s+\*\s+INTO\s+bak_[a-zA-Z0-9_]+_\d{12}\s+FROM\b",
|
|
221
|
+
stmt.text,
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
):
|
|
224
|
+
findings.append(
|
|
225
|
+
Finding(
|
|
226
|
+
"ERROR",
|
|
227
|
+
stmt.line,
|
|
228
|
+
"备份表名必须形如 bak_<原表或业务缩写>_<yyyyMMddHHmm>。",
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
for match in re.finditer(r"\bEXISTS\b", stmt.text, re.IGNORECASE):
|
|
233
|
+
findings.append(
|
|
234
|
+
Finding(
|
|
235
|
+
"WARN",
|
|
236
|
+
line_of_offset(stmt.text, stmt.line, match.start()),
|
|
237
|
+
"SQL 可读性偏好:成员关系/半连接默认使用 IN,只有 IN 改变语义时才保留 EXISTS 并说明原因。",
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if re.search(r"\bUPDATE\b.+\bJOIN\b", compact, re.IGNORECASE):
|
|
242
|
+
findings.append(
|
|
243
|
+
Finding(
|
|
244
|
+
"WARN",
|
|
245
|
+
stmt.line,
|
|
246
|
+
"PostgreSQL 多表更新优先使用 UPDATE ... FROM ... WHERE ...,不要使用 MySQL 风格 UPDATE ... JOIN。",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if re.search(r"=\s*NULL\b|\bNULL\s*=", stmt.text, re.IGNORECASE):
|
|
251
|
+
findings.append(
|
|
252
|
+
Finding(
|
|
253
|
+
"ERROR",
|
|
254
|
+
stmt.line,
|
|
255
|
+
"NULL 判断必须使用 IS NULL / IS NOT NULL,不能使用 = NULL。",
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if re.search(r"<>|!=", stmt.text) and has_token(stmt.text, "NULL"):
|
|
260
|
+
findings.append(
|
|
261
|
+
Finding(
|
|
262
|
+
"WARN",
|
|
263
|
+
stmt.line,
|
|
264
|
+
"涉及 NULL 的不等比较需确认语义;PostgreSQL 可优先使用 IS DISTINCT FROM。",
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return findings
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def lint_timestamps(path: Path, raw_sql: str) -> list[Finding]:
|
|
272
|
+
findings: list[Finding] = []
|
|
273
|
+
backup_timestamps = set(BACKUP_TABLE_RE.findall(raw_sql))
|
|
274
|
+
|
|
275
|
+
if len(backup_timestamps) > 1:
|
|
276
|
+
findings.append(
|
|
277
|
+
Finding(
|
|
278
|
+
"ERROR",
|
|
279
|
+
1,
|
|
280
|
+
"同一 SQL 文件中出现多个备份表时间戳;桌面文件、备份表和文件头时间戳必须一致。",
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
file_match = FILE_TS_RE.search(path.name)
|
|
285
|
+
if file_match and backup_timestamps and file_match.group(1) not in backup_timestamps:
|
|
286
|
+
findings.append(
|
|
287
|
+
Finding(
|
|
288
|
+
"ERROR",
|
|
289
|
+
1,
|
|
290
|
+
f"文件名时间戳 {file_match.group(1)} 与备份表时间戳 {', '.join(sorted(backup_timestamps))} 不一致。",
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
header_matches = list(HEADER_TS_RE.finditer(raw_sql))
|
|
295
|
+
header_timestamps = {m.group(1) for m in header_matches}
|
|
296
|
+
if len(header_timestamps) > 1:
|
|
297
|
+
findings.append(Finding("ERROR", 1, "文件头出现多个不同的备份表时间戳。"))
|
|
298
|
+
if header_timestamps and backup_timestamps and header_timestamps != backup_timestamps:
|
|
299
|
+
findings.append(
|
|
300
|
+
Finding(
|
|
301
|
+
"ERROR",
|
|
302
|
+
1,
|
|
303
|
+
f"文件头时间戳 {', '.join(sorted(header_timestamps))} 与备份表时间戳 {', '.join(sorted(backup_timestamps))} 不一致。",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return findings
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def lint_file(path: Path) -> list[Finding]:
|
|
311
|
+
raw_sql = path.read_text(encoding="utf-8")
|
|
312
|
+
masked_sql = strip_comments_and_literals(raw_sql)
|
|
313
|
+
|
|
314
|
+
findings: list[Finding] = []
|
|
315
|
+
findings.extend(lint_timestamps(path, raw_sql))
|
|
316
|
+
for stmt in iter_statements(masked_sql):
|
|
317
|
+
findings.extend(lint_statement(stmt))
|
|
318
|
+
return sorted(findings, key=lambda f: (f.line, f.severity, f.message))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
|
|
322
|
+
parser = argparse.ArgumentParser(
|
|
323
|
+
description="Lint ok-ksql generated PostgreSQL data-fix SQL files."
|
|
324
|
+
)
|
|
325
|
+
parser.add_argument("paths", nargs="+", type=Path, help="SQL/.txt files to lint")
|
|
326
|
+
parser.add_argument(
|
|
327
|
+
"--strict",
|
|
328
|
+
action="store_true",
|
|
329
|
+
help="treat WARN findings as failures",
|
|
330
|
+
)
|
|
331
|
+
return parser.parse_args(argv)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
335
|
+
args = parse_args(sys.argv[1:] if argv is None else argv)
|
|
336
|
+
all_findings: list[tuple[Path, Finding]] = []
|
|
337
|
+
|
|
338
|
+
for path in args.paths:
|
|
339
|
+
if not path.exists():
|
|
340
|
+
print(f"{path}:1: ERROR: 文件不存在。", file=sys.stderr)
|
|
341
|
+
return 2
|
|
342
|
+
if path.is_dir():
|
|
343
|
+
print(f"{path}:1: ERROR: 请输入 SQL/.txt 文件,不能是目录。", file=sys.stderr)
|
|
344
|
+
return 2
|
|
345
|
+
for finding in lint_file(path):
|
|
346
|
+
all_findings.append((path, finding))
|
|
347
|
+
|
|
348
|
+
for path, finding in all_findings:
|
|
349
|
+
print(finding.format(path))
|
|
350
|
+
|
|
351
|
+
error_count = sum(1 for _, f in all_findings if f.severity == "ERROR")
|
|
352
|
+
warn_count = sum(1 for _, f in all_findings if f.severity == "WARN")
|
|
353
|
+
print(f"SUMMARY: {error_count} error(s), {warn_count} warning(s)")
|
|
354
|
+
|
|
355
|
+
if error_count:
|
|
356
|
+
return 1
|
|
357
|
+
if args.strict and warn_count:
|
|
358
|
+
return 1
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
raise SystemExit(main())
|