atquery 2.13.0__tar.gz → 2.13.2__tar.gz
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.
- {atquery-2.13.0 → atquery-2.13.2}/PKG-INFO +1 -1
- {atquery-2.13.0 → atquery-2.13.2}/pyproject.toml +2 -2
- {atquery-2.13.0 → atquery-2.13.2}/pyproject.toml.orig +2 -2
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/ecd.py +56 -1
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/main.py +10 -1
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/__init__.py +2 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/model.py +6 -1
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/parser.py +8 -2
- atquery-2.13.2/src/atquery/ecd/seed.py +217 -0
- {atquery-2.13.0 → atquery-2.13.2}/LICENSE +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/README.md +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/aio/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/aio/engine.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/aio/query.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/api_v2.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/atquery2.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/columns.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/ddl.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/diff.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/migrate.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/routines.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/scenario.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/schemas.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/cli/tables.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/duckdb.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/aio.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/core.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/dump/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/dump/__main__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/dump/core.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/meta.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/pooling.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/restore/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/restore/__main__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/mysql/restore/core.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/dialect/sqlite.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/diff/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/diff/comparator.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/diff/generator.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/diff/git_integration.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/diff/models.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/diff/parser.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/_toml.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/catalog.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/ddl.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/diff.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/erd.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/errors.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/resolver.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/ecd/skeleton.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/engine.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/factory.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/migration/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/migration/_migrator.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/migration/_protocol.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/migration/_standard.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/migration/_types.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/py.typed +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/testing/__init__.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/testing/creator.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/testing/loader.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/testing/schema.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/testing/validator.py +0 -0
- {atquery-2.13.0 → atquery-2.13.2}/src/atquery/type.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["uv_build>=0.11.15
|
|
2
|
+
requires = ["uv_build>=0.11.15"]
|
|
3
3
|
build-backend = "uv_build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "atquery"
|
|
7
|
-
version = "2.13.
|
|
7
|
+
version = "2.13.2"
|
|
8
8
|
description = "A Python module for simplifying DBAPI usage with one-query-per-file SQL management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["uv_build>=0.11.15
|
|
2
|
+
requires = ["uv_build>=0.11.15"]
|
|
3
3
|
build-backend = "uv_build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "atquery"
|
|
7
|
-
version = "2.13.
|
|
7
|
+
version = "2.13.2"
|
|
8
8
|
description = "A Python module for simplifying DBAPI usage with one-query-per-file SQL management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
# ECDファイルからCREATE文を生成(ディレクトリ指定は配下を再帰的に探索)
|
|
10
10
|
atquery ecd ddl docs/ecd -o schema.sql
|
|
11
11
|
|
|
12
|
+
# 列挙値マスタの`values`からUPSERT文(初期データ投入SQL)を生成
|
|
13
|
+
atquery ecd seed docs/ecd -o seed.sql --dialect mysql
|
|
14
|
+
|
|
15
|
+
# 監査列(*_by等)に全テーブル共通の固定値を注入する
|
|
16
|
+
atquery ecd seed docs/ecd -o seed.sql --column created_by "rev 1" --column created_id "tx-232311"
|
|
17
|
+
|
|
12
18
|
# glob表現で複数ファイルをまとめて指定
|
|
13
19
|
atquery ecd ddl "docs/ecd/**/*.md" -o schema.sql
|
|
14
20
|
|
|
@@ -70,6 +76,25 @@ def setup_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
70
76
|
_add_ecd_source_args(erd_parser)
|
|
71
77
|
erd_parser.add_argument("-o", "--output", help="出力先ファイル(省略時は標準出力)")
|
|
72
78
|
|
|
79
|
+
seed_parser = subparsers.add_parser(
|
|
80
|
+
"seed",
|
|
81
|
+
help="Generate UPSERT statements from ECD `values` (enum master seed data)",
|
|
82
|
+
)
|
|
83
|
+
_add_ecd_source_args(seed_parser)
|
|
84
|
+
seed_parser.add_argument("-o", "--output", help="出力先ファイル(省略時は標準出力)")
|
|
85
|
+
seed_parser.add_argument(
|
|
86
|
+
"--dialect", default="mysql", choices=["mysql", "sqlite"], help="出力するSQL方言(既定: mysql)"
|
|
87
|
+
)
|
|
88
|
+
seed_parser.add_argument(
|
|
89
|
+
"--column",
|
|
90
|
+
nargs=2,
|
|
91
|
+
metavar=("NAME", "VALUE"),
|
|
92
|
+
action="append",
|
|
93
|
+
dest="columns",
|
|
94
|
+
help="全テーブル共通の固定値を持つ列を追加指定する(監査列`created_by`等向け、複数回指定可)。"
|
|
95
|
+
"対象列が存在しないテーブルには書き込まれない",
|
|
96
|
+
)
|
|
97
|
+
|
|
73
98
|
catalog_parser = subparsers.add_parser(
|
|
74
99
|
"catalog",
|
|
75
100
|
help="Generate a Markdown catalog (summary + details) from ECD files",
|
|
@@ -139,12 +164,14 @@ def execute_ecd(args: argparse.Namespace) -> int:
|
|
|
139
164
|
return _execute_ddl(args)
|
|
140
165
|
if command == "erd":
|
|
141
166
|
return _execute_erd(args)
|
|
167
|
+
if command == "seed":
|
|
168
|
+
return _execute_seed(args)
|
|
142
169
|
if command == "catalog":
|
|
143
170
|
return _execute_catalog(args)
|
|
144
171
|
if command == "diff":
|
|
145
172
|
return _execute_diff(args)
|
|
146
173
|
|
|
147
|
-
print("Usage: atquery ecd <skeleton|ddl|erd|catalog|diff> ...", file=sys.stderr)
|
|
174
|
+
print("Usage: atquery ecd <skeleton|ddl|erd|seed|catalog|diff> ...", file=sys.stderr)
|
|
148
175
|
print(" atquery ecd --help", file=sys.stderr)
|
|
149
176
|
return EXIT_ERROR
|
|
150
177
|
|
|
@@ -235,6 +262,34 @@ def _execute_ddl(args: argparse.Namespace) -> int:
|
|
|
235
262
|
return EXIT_SUCCESS
|
|
236
263
|
|
|
237
264
|
|
|
265
|
+
def _execute_seed(args: argparse.Namespace) -> int:
|
|
266
|
+
documents = _load_ecd_documents(args)
|
|
267
|
+
if documents is None:
|
|
268
|
+
return EXIT_ERROR
|
|
269
|
+
|
|
270
|
+
from atquery.ecd import ECDError, generate_seed_sql, resolve_documents
|
|
271
|
+
|
|
272
|
+
static_columns = dict(args.columns) if getattr(args, "columns", None) else {}
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
sql, warnings = generate_seed_sql(
|
|
276
|
+
resolve_documents(documents), dialect=args.dialect, static_columns=static_columns
|
|
277
|
+
)
|
|
278
|
+
except ECDError as e:
|
|
279
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
280
|
+
return EXIT_ERROR
|
|
281
|
+
|
|
282
|
+
for warning in warnings:
|
|
283
|
+
print(f"Warning: {warning}", file=sys.stderr)
|
|
284
|
+
|
|
285
|
+
if not sql.strip():
|
|
286
|
+
print("No `values` definitions found.", file=sys.stderr)
|
|
287
|
+
return EXIT_SUCCESS
|
|
288
|
+
|
|
289
|
+
_write_output(sql, args.output)
|
|
290
|
+
return EXIT_SUCCESS
|
|
291
|
+
|
|
292
|
+
|
|
238
293
|
def _execute_erd(args: argparse.Namespace) -> int:
|
|
239
294
|
documents = _load_ecd_documents(args)
|
|
240
295
|
if documents is None:
|
|
@@ -273,6 +273,12 @@ class OutputWriter:
|
|
|
273
273
|
|
|
274
274
|
def write_header(self, keys: Sequence[str]):
|
|
275
275
|
"""ヘッダーを出力"""
|
|
276
|
+
# DBのJSON型カラムはPython API (get()/getall()) と同じ基準で自動デコード対象に加える
|
|
277
|
+
# (column_type == "JSON")。tables.py等が明示指定するjson_columns(JSON文字列だが型はJSONでない
|
|
278
|
+
# カラム)はこれとは別に維持されるため、ここでは追加(|=)のみ行う。
|
|
279
|
+
if self.column_types:
|
|
280
|
+
self.json_columns |= {name for name, type_ in zip(keys, self.column_types) if type_ == "JSON"}
|
|
281
|
+
|
|
276
282
|
if self.output_format == "json":
|
|
277
283
|
if self.with_types:
|
|
278
284
|
columns = self._columns_metadata(keys)
|
|
@@ -782,7 +788,8 @@ Examples:
|
|
|
782
788
|
with datasource.connect() as conn:
|
|
783
789
|
result = explain_query.execute(conn, params=params)
|
|
784
790
|
# 検証のみ実行、結果は破棄
|
|
785
|
-
result.fetchall()
|
|
791
|
+
plan_rows = result.fetchall()
|
|
792
|
+
print(f"OK: dry-run succeeded (validated via EXPLAIN, {len(plan_rows)} plan row(s), no data was modified)", file=sys.stderr)
|
|
786
793
|
return EXIT_SUCCESS
|
|
787
794
|
else:
|
|
788
795
|
# INSERT/UPDATE/DELETE文の場合はPREPAREで構文チェック(MySQL限定)
|
|
@@ -798,10 +805,12 @@ Examples:
|
|
|
798
805
|
conn.execute(prepare_sql)
|
|
799
806
|
# 正常に準備できたらDEALLOCATE
|
|
800
807
|
conn.execute(f"DEALLOCATE PREPARE {stmt_name}")
|
|
808
|
+
print("OK: dry-run succeeded (validated via PREPARE, no data was modified)", file=sys.stderr)
|
|
801
809
|
return EXIT_SUCCESS
|
|
802
810
|
except AttributeError:
|
|
803
811
|
# MySQL以外のDBの場合は構文チェック不可
|
|
804
812
|
# パラメータバインドまで成功していればOKとする
|
|
813
|
+
print("OK: dry-run succeeded (parameters bound; syntax check unavailable for this DBMS, no data was modified)", file=sys.stderr)
|
|
805
814
|
return EXIT_SUCCESS
|
|
806
815
|
|
|
807
816
|
# 7. クエリの実行
|
|
@@ -28,6 +28,7 @@ from atquery.ecd.model import (
|
|
|
28
28
|
)
|
|
29
29
|
from atquery.ecd.parser import is_ecd_document, parse_file, parse_text
|
|
30
30
|
from atquery.ecd.resolver import ResolvedTable, resolve_documents
|
|
31
|
+
from atquery.ecd.seed import generate_seed_sql
|
|
31
32
|
from atquery.ecd.skeleton import generate_skeleton
|
|
32
33
|
|
|
33
34
|
__all__ = [
|
|
@@ -52,6 +53,7 @@ __all__ = [
|
|
|
52
53
|
"generate_catalog_markdown",
|
|
53
54
|
"generate_diff_sql",
|
|
54
55
|
"generate_erd_svg",
|
|
56
|
+
"generate_seed_sql",
|
|
55
57
|
"generate_skeleton",
|
|
56
58
|
"generate_table_ddl",
|
|
57
59
|
"generate_view_ddl",
|
|
@@ -18,10 +18,15 @@ class Frontmatter:
|
|
|
18
18
|
|
|
19
19
|
@dataclass(frozen=True)
|
|
20
20
|
class CodeValue:
|
|
21
|
-
"""`values` 配列の要素(コード値定義)。
|
|
21
|
+
"""`values` 配列の要素(コード値定義)。
|
|
22
|
+
|
|
23
|
+
`code`/`label` 以外のキー(`sort_order`/`description`等)は`extra`に汎用的に集約する。
|
|
24
|
+
仕様: docs/dev/adr/0037-ecd-enum-master-seed-and-value-metadata.md
|
|
25
|
+
"""
|
|
22
26
|
|
|
23
27
|
code: object
|
|
24
28
|
label: str
|
|
29
|
+
extra: dict[str, object] = field(default_factory=dict)
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
@dataclass(frozen=True)
|
|
@@ -205,7 +205,7 @@ def _build_view_block(path: str, meta: dict, sql_blocks: dict[str, str]) -> View
|
|
|
205
205
|
|
|
206
206
|
|
|
207
207
|
def _build_column_def(name: str, table: dict) -> ColumnDef:
|
|
208
|
-
values = tuple(
|
|
208
|
+
values = tuple(_build_code_value(v) for v in table.get("values", ()))
|
|
209
209
|
return ColumnDef(
|
|
210
210
|
name=name,
|
|
211
211
|
logical_name=table.get("logical_name"),
|
|
@@ -215,10 +215,16 @@ def _build_column_def(name: str, table: dict) -> ColumnDef:
|
|
|
215
215
|
)
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def _build_code_value(v: dict) -> CodeValue:
|
|
219
|
+
"""`values`配列の1要素をパースする。`code`/`label`以外のキーは`extra`へ集約する。"""
|
|
220
|
+
extra = {k: val for k, val in v.items() if k not in ("code", "label")}
|
|
221
|
+
return CodeValue(code=v["code"], label=v["label"], extra=extra)
|
|
222
|
+
|
|
223
|
+
|
|
218
224
|
def _build_column_ref(name: str, table: dict) -> ColumnRef:
|
|
219
225
|
values = None
|
|
220
226
|
if "values" in table:
|
|
221
|
-
values = tuple(
|
|
227
|
+
values = tuple(_build_code_value(v) for v in table["values"])
|
|
222
228
|
return ColumnRef(
|
|
223
229
|
name=name,
|
|
224
230
|
block_name=table["ref"][len("columns:") :],
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""`values`(コード値定義)から初期データ投入用のUPSERT文を生成する(`atquery ecd seed`)。
|
|
2
|
+
|
|
3
|
+
仕様: docs/dev/adr/0037-ecd-enum-master-seed-and-value-metadata.md
|
|
4
|
+
|
|
5
|
+
- 対象テーブル: いずれかの列が非空の`values`を持つテーブル(1テーブルにつき`values`を持つ列は1つの前提)。
|
|
6
|
+
- 列マッピング: `code`/`label`/`extra`のキーを問わず、同名の列がテーブルに実在する場合のみ書き込む
|
|
7
|
+
(エイリアスは無し。未マッピングの`extra`キーは警告を返す)。
|
|
8
|
+
- 行の並び順: 全エントリに`extra["sort_order"]`があればそれで昇順ソート、無ければ宣言順。
|
|
9
|
+
SQLリテラルへの型変換: マッピング先の列の`sql_def`(DDL型)を見て決める(`code`/`label`にも共通適用)。
|
|
10
|
+
- SQL種別: UPSERT(MySQL: `ON DUPLICATE KEY UPDATE`、SQLite: `ON CONFLICT(...) DO UPDATE SET`)。
|
|
11
|
+
UPSERTのキーはテーブルの`primary_keys`宣言をそのまま用いる(複合PKも全列をキーにする)。
|
|
12
|
+
- 監査列など`values`由来では埋められない列は`static_columns`(CLIの`--column NAME VALUE`)で
|
|
13
|
+
全テーブル共通の固定値として注入できる。対象列が無いテーブルはサイレントにスキップする(
|
|
14
|
+
複数テーブルへのブロードキャストを前提とした仕様のため)。`values`由来の列名と衝突する場合は
|
|
15
|
+
`--column`側を無視して警告する。INSERT/UPDATE両方に一律で含める(更新を防ぎたい場合は利用者が
|
|
16
|
+
別途差分migrationを作成する想定)。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from atquery.ecd.model import CodeValue, ColumnDef
|
|
24
|
+
from atquery.ecd.resolver import ResolvedTable
|
|
25
|
+
|
|
26
|
+
# MySQLの裸識別子として許可される文字(ddl.pyの規則に合わせる)
|
|
27
|
+
_SAFE_BARE_IDENTIFIER_RE = re.compile(r"^[0-9A-Za-z_$\u0080-\U0010ffff]+$")
|
|
28
|
+
|
|
29
|
+
_TEXT_SQL_TYPES = {"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", "ENUM", "SET"}
|
|
30
|
+
_DATE_SQL_TYPES = {"DATE", "DATETIME", "TIMESTAMP", "TIME", "YEAR"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_seed_sql(
|
|
34
|
+
tables: Iterable[ResolvedTable], *, dialect: str = "mysql", static_columns: dict[str, str] | None = None
|
|
35
|
+
) -> tuple[str, list[str]]:
|
|
36
|
+
"""`values`を持つ全テーブル分のUPSERT文を生成する。
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
tables: 対象の`ResolvedTable`一覧
|
|
40
|
+
dialect: 出力するSQL方言("mysql" | "sqlite")
|
|
41
|
+
static_columns: `--column NAME VALUE`で指定された全テーブル共通の固定値
|
|
42
|
+
(例: 監査列`created_by`/`created_id`)。対象列が無いテーブルはサイレントにスキップする。
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
tuple: (UPSERT文を`\\n\\n`で連結したSQL文字列, 警告メッセージのリスト)
|
|
46
|
+
"""
|
|
47
|
+
sql_parts: list[str] = []
|
|
48
|
+
warnings: list[str] = []
|
|
49
|
+
|
|
50
|
+
for table in tables:
|
|
51
|
+
result = _generate_table_seed(table, dialect=dialect, static_columns=static_columns or {})
|
|
52
|
+
if result is None:
|
|
53
|
+
continue
|
|
54
|
+
sql, table_warnings = result
|
|
55
|
+
sql_parts.append(sql)
|
|
56
|
+
warnings.extend(table_warnings)
|
|
57
|
+
|
|
58
|
+
return "\n\n".join(sql_parts), warnings
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _generate_table_seed(
|
|
62
|
+
table: ResolvedTable, *, dialect: str, static_columns: dict[str, str]
|
|
63
|
+
) -> tuple[str, list[str]] | None:
|
|
64
|
+
values_col = next((col for col in table.columns if col.values), None)
|
|
65
|
+
if values_col is None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
columns_by_name = {col.name: col for col in table.columns}
|
|
69
|
+
mapped_names, static_values, warnings = _map_columns(table.name, values_col, columns_by_name, static_columns)
|
|
70
|
+
rows = _ordered_rows(values_col.values)
|
|
71
|
+
|
|
72
|
+
value_rows = [_render_row(values_col, entry, mapped_names, columns_by_name, static_values) for entry in rows]
|
|
73
|
+
key_columns = list(table.primary_keys) or [values_col.name]
|
|
74
|
+
|
|
75
|
+
if dialect == "sqlite":
|
|
76
|
+
sql = _render_sqlite_upsert(table.name, mapped_names, value_rows, key_columns)
|
|
77
|
+
else:
|
|
78
|
+
sql = _render_mysql_upsert(table.name, mapped_names, value_rows, key_columns)
|
|
79
|
+
return sql, warnings
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _map_columns(
|
|
83
|
+
table_name: str,
|
|
84
|
+
values_col: ColumnDef,
|
|
85
|
+
columns_by_name: dict[str, ColumnDef],
|
|
86
|
+
static_columns: dict[str, str],
|
|
87
|
+
) -> tuple[list[str], dict[str, str], list[str]]:
|
|
88
|
+
"""`code`列自身 + `label`/`extra`のキーのうち、同名の列が実在するものだけをマッピングする。
|
|
89
|
+
さらに`static_columns`(`--column`)を、values由来の列名と衝突しない範囲で追加する。
|
|
90
|
+
"""
|
|
91
|
+
mapped = [values_col.name]
|
|
92
|
+
warnings: list[str] = []
|
|
93
|
+
|
|
94
|
+
if "label" in columns_by_name:
|
|
95
|
+
mapped.append("label")
|
|
96
|
+
|
|
97
|
+
extra_keys: dict[str, None] = {}
|
|
98
|
+
for entry in values_col.values:
|
|
99
|
+
for key in entry.extra:
|
|
100
|
+
extra_keys.setdefault(key, None)
|
|
101
|
+
|
|
102
|
+
for key in extra_keys:
|
|
103
|
+
if key in columns_by_name:
|
|
104
|
+
mapped.append(key)
|
|
105
|
+
else:
|
|
106
|
+
warnings.append(
|
|
107
|
+
f"table {table_name!r}: values の extra キー {key!r} に対応する列が見つからないため無視します"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
static_values: dict[str, str] = {}
|
|
111
|
+
for name, value in static_columns.items():
|
|
112
|
+
if name in mapped:
|
|
113
|
+
warnings.append(
|
|
114
|
+
f"table {table_name!r}: --column {name!r} は values 由来の列と同名のため無視します"
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
if name in columns_by_name:
|
|
118
|
+
mapped.append(name)
|
|
119
|
+
static_values[name] = value
|
|
120
|
+
# 対象テーブルに列が無ければサイレントにスキップ(複数テーブルへのブロードキャストを前提)
|
|
121
|
+
|
|
122
|
+
return mapped, static_values, warnings
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _ordered_rows(values: tuple[CodeValue, ...]) -> list[CodeValue]:
|
|
126
|
+
if all("sort_order" in v.extra for v in values):
|
|
127
|
+
return sorted(values, key=_sort_order_key)
|
|
128
|
+
return list(values)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _sort_order_key(v: CodeValue) -> Any:
|
|
132
|
+
return v.extra["sort_order"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _render_row(
|
|
136
|
+
values_col: ColumnDef,
|
|
137
|
+
entry: CodeValue,
|
|
138
|
+
mapped_names: list[str],
|
|
139
|
+
columns_by_name: dict[str, ColumnDef],
|
|
140
|
+
static_values: dict[str, str],
|
|
141
|
+
) -> list[str]:
|
|
142
|
+
literals = []
|
|
143
|
+
for name in mapped_names:
|
|
144
|
+
if name == values_col.name:
|
|
145
|
+
value = entry.code
|
|
146
|
+
elif name == "label":
|
|
147
|
+
value = entry.label
|
|
148
|
+
elif name in static_values:
|
|
149
|
+
value = static_values[name]
|
|
150
|
+
else:
|
|
151
|
+
value = entry.extra.get(name)
|
|
152
|
+
literals.append(_render_literal(value, columns_by_name[name].sql_def))
|
|
153
|
+
return literals
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _render_literal(value: object, sql_def: str) -> str:
|
|
157
|
+
if value is None:
|
|
158
|
+
return "NULL"
|
|
159
|
+
base_type = _base_sql_type(sql_def)
|
|
160
|
+
if isinstance(value, bool):
|
|
161
|
+
return "1" if value else "0"
|
|
162
|
+
if base_type in _TEXT_SQL_TYPES or base_type in _DATE_SQL_TYPES or isinstance(value, str):
|
|
163
|
+
return f"'{_escape(str(value))}'"
|
|
164
|
+
return str(value)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _base_sql_type(sql_def: str) -> str:
|
|
168
|
+
token = sql_def.split()[0] if sql_def else ""
|
|
169
|
+
return token.split("(")[0].upper()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _render_mysql_upsert(
|
|
173
|
+
table_name: str, columns: list[str], value_rows: list[list[str]], key_columns: list[str]
|
|
174
|
+
) -> str:
|
|
175
|
+
update_columns = [c for c in columns if c not in key_columns]
|
|
176
|
+
column_list = ", ".join(_quote_ident(c, dialect="mysql") for c in columns)
|
|
177
|
+
values_block = ",\n".join(f" ({', '.join(row)})" for row in value_rows)
|
|
178
|
+
sql = f"INSERT INTO {_quote_ident(table_name, dialect='mysql')} ({column_list})\nVALUES\n{values_block}"
|
|
179
|
+
if update_columns:
|
|
180
|
+
update_clause = ",\n".join(
|
|
181
|
+
f" {_quote_ident(c, dialect='mysql')} = VALUES({_quote_ident(c, dialect='mysql')})"
|
|
182
|
+
for c in update_columns
|
|
183
|
+
)
|
|
184
|
+
sql += f"\nON DUPLICATE KEY UPDATE\n{update_clause}"
|
|
185
|
+
return sql + "\n;"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _render_sqlite_upsert(
|
|
189
|
+
table_name: str, columns: list[str], value_rows: list[list[str]], key_columns: list[str]
|
|
190
|
+
) -> str:
|
|
191
|
+
update_columns = [c for c in columns if c not in key_columns]
|
|
192
|
+
column_list = ", ".join(_quote_ident(c, dialect="sqlite") for c in columns)
|
|
193
|
+
values_block = ",\n".join(f" ({', '.join(row)})" for row in value_rows)
|
|
194
|
+
conflict_columns = ", ".join(_quote_ident(c, dialect="sqlite") for c in key_columns)
|
|
195
|
+
sql = f"INSERT INTO {_quote_ident(table_name, dialect='sqlite')} ({column_list})\nVALUES\n{values_block}"
|
|
196
|
+
sql += f"\nON CONFLICT({conflict_columns})"
|
|
197
|
+
if update_columns:
|
|
198
|
+
update_clause = ",\n".join(
|
|
199
|
+
f" {_quote_ident(c, dialect='sqlite')} = excluded.{_quote_ident(c, dialect='sqlite')}"
|
|
200
|
+
for c in update_columns
|
|
201
|
+
)
|
|
202
|
+
sql += f" DO UPDATE SET\n{update_clause}"
|
|
203
|
+
else:
|
|
204
|
+
sql += " DO NOTHING"
|
|
205
|
+
return sql + "\n;"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _quote_ident(name: str, *, dialect: str) -> str:
|
|
209
|
+
if _SAFE_BARE_IDENTIFIER_RE.match(name) and not name.isdigit():
|
|
210
|
+
return name
|
|
211
|
+
if dialect == "sqlite":
|
|
212
|
+
return f'"{name.replace(chr(34), chr(34) * 2)}"'
|
|
213
|
+
return f"`{name.replace('`', '``')}`"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _escape(s: str) -> str:
|
|
217
|
+
return s.replace("'", "''")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|