fraiseql-semis 0.1.0__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.
- fraiseql_semis-0.1.0/.gitignore +70 -0
- fraiseql_semis-0.1.0/CHANGELOG.md +41 -0
- fraiseql_semis-0.1.0/LICENSE +21 -0
- fraiseql_semis-0.1.0/PKG-INFO +370 -0
- fraiseql_semis-0.1.0/README.md +334 -0
- fraiseql_semis-0.1.0/docs/ARCHITECTURE.md +646 -0
- fraiseql_semis-0.1.0/docs/PRD.md +156 -0
- fraiseql_semis-0.1.0/pyproject.toml +138 -0
- fraiseql_semis-0.1.0/scenarios/minimal_seed.yaml +16 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/__init__.py +106 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/cli/__init__.py +5 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/cli/main.py +493 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/cli/options.py +110 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/codes.py +59 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/emit.py +154 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/errors.py +97 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/faker_provider.py +254 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/generator.py +416 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/hierarchy.py +100 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/pin.py +199 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/project.py +385 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/providers/__init__.py +17 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/providers/i18n.py +113 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/providers/organization.py +98 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/readback.py +98 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/resolution.py +167 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/rows.py +101 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/scenario.py +644 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/schema.py +211 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/seeds.py +100 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/staging.py +77 -0
- fraiseql_semis-0.1.0/src/fraiseql_semis/uuid_generator.py +65 -0
- fraiseql_semis-0.1.0/tests/__init__.py +0 -0
- fraiseql_semis-0.1.0/tests/conftest.py +26 -0
- fraiseql_semis-0.1.0/tests/contract/__init__.py +0 -0
- fraiseql_semis-0.1.0/tests/contract/test_apply_reads_a_tree.py +34 -0
- fraiseql_semis-0.1.0/tests/contract/test_column_facts.py +37 -0
- fraiseql_semis-0.1.0/tests/contract/test_copy_seed_is_checked.py +49 -0
- fraiseql_semis-0.1.0/tests/contract/test_database_url_precedence.py +106 -0
- fraiseql_semis-0.1.0/tests/contract/test_diff_reads_sources.py +49 -0
- fraiseql_semis-0.1.0/tests/contract/test_exit_codes.py +41 -0
- fraiseql_semis-0.1.0/tests/contract/test_final_table_per_resolver.py +52 -0
- fraiseql_semis-0.1.0/tests/contract/test_level_1_uuid_check.py +24 -0
- fraiseql_semis-0.1.0/tests/contract/test_model_json_is_not_cross_source_stable.py +69 -0
- fraiseql_semis-0.1.0/tests/contract/test_parse_schema.py +32 -0
- fraiseql_semis-0.1.0/tests/contract/test_platform_surface.py +49 -0
- fraiseql_semis-0.1.0/tests/contract/test_prep_seed_convention.py +60 -0
- fraiseql_semis-0.1.0/tests/contract/test_prep_seed_level_3.py +87 -0
- fraiseql_semis-0.1.0/tests/contract/test_prep_seed_level_3_unreferenced.py +74 -0
- fraiseql_semis-0.1.0/tests/contract/test_prep_seed_level_5.py +81 -0
- fraiseql_semis-0.1.0/tests/contract/test_self_reference.py +23 -0
- fraiseql_semis-0.1.0/tests/contract/test_transaction_ownership.py +64 -0
- fraiseql_semis-0.1.0/tests/contract/test_writer_refusals.py +39 -0
- fraiseql_semis-0.1.0/tests/contract/test_writers_do_not_validate.py +46 -0
- fraiseql_semis-0.1.0/tests/ddl.py +115 -0
- fraiseql_semis-0.1.0/tests/docs/__init__.py +0 -0
- fraiseql_semis-0.1.0/tests/docs/test_examples.py +663 -0
- fraiseql_semis-0.1.0/tests/integration/__init__.py +0 -0
- fraiseql_semis-0.1.0/tests/integration/test_cli_apply.py +168 -0
- fraiseql_semis-0.1.0/tests/integration/test_hierarchy.py +109 -0
- fraiseql_semis-0.1.0/tests/integration/test_one_table_end_to_end.py +66 -0
- fraiseql_semis-0.1.0/tests/integration/test_providers.py +93 -0
- fraiseql_semis-0.1.0/tests/integration/test_read_back.py +147 -0
- fraiseql_semis-0.1.0/tests/integration/test_scale.py +65 -0
- fraiseql_semis-0.1.0/tests/integration/test_scenario_execution.py +148 -0
- fraiseql_semis-0.1.0/tests/integration/test_validate_seeds.py +190 -0
- fraiseql_semis-0.1.0/tests/performance/__init__.py +0 -0
- fraiseql_semis-0.1.0/tests/performance/test_generation_rate.py +138 -0
- fraiseql_semis-0.1.0/tests/unit/__init__.py +0 -0
- fraiseql_semis-0.1.0/tests/unit/fake_providers.py +21 -0
- fraiseql_semis-0.1.0/tests/unit/guards.py +25 -0
- fraiseql_semis-0.1.0/tests/unit/test_cli.py +555 -0
- fraiseql_semis-0.1.0/tests/unit/test_cli_has_no_generation.py +83 -0
- fraiseql_semis-0.1.0/tests/unit/test_cli_options.py +40 -0
- fraiseql_semis-0.1.0/tests/unit/test_codes.py +48 -0
- fraiseql_semis-0.1.0/tests/unit/test_emit.py +63 -0
- fraiseql_semis-0.1.0/tests/unit/test_errors.py +30 -0
- fraiseql_semis-0.1.0/tests/unit/test_faker_provider.py +188 -0
- fraiseql_semis-0.1.0/tests/unit/test_generator.py +111 -0
- fraiseql_semis-0.1.0/tests/unit/test_hierarchy.py +200 -0
- fraiseql_semis-0.1.0/tests/unit/test_nothing_is_evaluated.py +55 -0
- fraiseql_semis-0.1.0/tests/unit/test_one_code_registry.py +64 -0
- fraiseql_semis-0.1.0/tests/unit/test_one_confiture_seam.py +36 -0
- fraiseql_semis-0.1.0/tests/unit/test_one_sql_site.py +74 -0
- fraiseql_semis-0.1.0/tests/unit/test_package.py +51 -0
- fraiseql_semis-0.1.0/tests/unit/test_pin.py +141 -0
- fraiseql_semis-0.1.0/tests/unit/test_project.py +385 -0
- fraiseql_semis-0.1.0/tests/unit/test_providers.py +229 -0
- fraiseql_semis-0.1.0/tests/unit/test_pure_modules.py +37 -0
- fraiseql_semis-0.1.0/tests/unit/test_readback.py +40 -0
- fraiseql_semis-0.1.0/tests/unit/test_resolution.py +208 -0
- fraiseql_semis-0.1.0/tests/unit/test_rows.py +118 -0
- fraiseql_semis-0.1.0/tests/unit/test_scenario.py +241 -0
- fraiseql_semis-0.1.0/tests/unit/test_scenario_run.py +283 -0
- fraiseql_semis-0.1.0/tests/unit/test_schema_facts.py +73 -0
- fraiseql_semis-0.1.0/tests/unit/test_seeds.py +94 -0
- fraiseql_semis-0.1.0/tests/unit/test_shipped_scenarios.py +26 -0
- fraiseql_semis-0.1.0/tests/unit/test_staging.py +110 -0
- fraiseql_semis-0.1.0/tests/unit/test_toolchain.py +55 -0
- fraiseql_semis-0.1.0/tests/unit/test_uuid_generator.py +62 -0
- fraiseql_semis-0.1.0/tests/unit/test_validate_seeds.py +45 -0
- fraiseql_semis-0.1.0/tests/unit/test_warehouse.py +93 -0
- fraiseql_semis-0.1.0/tests/warehouse/scenarios/stock.yaml +39 -0
- fraiseql_semis-0.1.0/tests/warehouse/schema/010_parents.sql +11 -0
- fraiseql_semis-0.1.0/tests/warehouse/schema/020_catalog.sql +98 -0
- fraiseql_semis-0.1.0/tests/warehouse/schema/030_prep_seed.sql +92 -0
- fraiseql_semis-0.1.0/tests/warehouse/semis.yaml +14 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
share/python-wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Virtual environments
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
env/
|
|
30
|
+
.venv
|
|
31
|
+
|
|
32
|
+
# IDE
|
|
33
|
+
.vscode/
|
|
34
|
+
.idea/
|
|
35
|
+
*.swp
|
|
36
|
+
*.swo
|
|
37
|
+
*~
|
|
38
|
+
.DS_Store
|
|
39
|
+
|
|
40
|
+
# Testing
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.nox/
|
|
47
|
+
|
|
48
|
+
# Type checking
|
|
49
|
+
.pytype/
|
|
50
|
+
|
|
51
|
+
# Ruff
|
|
52
|
+
.ruff_cache/
|
|
53
|
+
|
|
54
|
+
# Documentation
|
|
55
|
+
docs/_build/
|
|
56
|
+
site/
|
|
57
|
+
|
|
58
|
+
# Local configuration
|
|
59
|
+
*.local.yaml
|
|
60
|
+
*.local.yml
|
|
61
|
+
.env
|
|
62
|
+
.env.local
|
|
63
|
+
|
|
64
|
+
# Database
|
|
65
|
+
*.db
|
|
66
|
+
*.sqlite
|
|
67
|
+
*.sqlite3
|
|
68
|
+
|
|
69
|
+
# Logs
|
|
70
|
+
*.log
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to fraiseql-semis are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-09-30
|
|
8
|
+
|
|
9
|
+
The first release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Semantic UUIDs**: each row's `id` encodes its table code, scenario id, version and
|
|
14
|
+
sequence; `semis decode-uuid` reads one back, naming the table and the scenario.
|
|
15
|
+
- **Values from the schema's facts**, read through `confiture.platform`: type, declared
|
|
16
|
+
length, enum labels, NOT NULL and UNIQUE; foreign keys from their `REFERENCES`.
|
|
17
|
+
- **Two declared FK modes**: prep-seed (a child carries its parent's UUID, written into
|
|
18
|
+
the staging twin) and read-back (parents applied, their `pk_*` read back by UUID).
|
|
19
|
+
- **The row contract**: a row missing a NOT NULL column without a default is refused,
|
|
20
|
+
with a per-column `trusts_trigger:` opt-out.
|
|
21
|
+
- **YAML scenarios** with overrides (a value, a list, a callable, or `null` for a
|
|
22
|
+
nullable key), per-column providers, and hierarchies for self-referencing tables,
|
|
23
|
+
including `ltree` paths in read-back.
|
|
24
|
+
- **Schema pins**: a digest of the facts semis consumes, refusing a replay against a
|
|
25
|
+
schema that has moved and reporting what changed.
|
|
26
|
+
- **Provider libraries**: `i18n` and `organization` shipped; a project's own providers
|
|
27
|
+
as `module:attribute`; installed libraries enabled by name through the
|
|
28
|
+
`fraiseql_semis.providers` entry point.
|
|
29
|
+
- **The `semis` command**: `seeds`, `apply`, `generate`, `table`, `validate`,
|
|
30
|
+
`validate-seeds` (confiture's five prep-seed levels), `list-scenarios`,
|
|
31
|
+
`init-scenario` and `decode-uuid`, configured by `semis.yaml`.
|
|
32
|
+
- **Scale**: 21,500 rows per second generated; 100,000 rows written and applied in 15
|
|
33
|
+
seconds, streamed rather than held.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- **A database URL's password is never printed.** semis masks it in whatever a refusal
|
|
38
|
+
prints, and requires fraiseql-confiture 1.27, whose `CONFIG_003` refusal of a
|
|
39
|
+
`--database-url` no longer repeats the password either.
|
|
40
|
+
|
|
41
|
+
[0.1.0]: https://github.com/fraiseql/semis/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lionel Hamayon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fraiseql-semis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Semantic UUID-encoded fake data generator for PostgreSQL trinity pattern schemas
|
|
5
|
+
Project-URL: Homepage, https://github.com/fraiseql/semis
|
|
6
|
+
Project-URL: Documentation, https://fraiseql.dev/semis
|
|
7
|
+
Project-URL: Repository, https://github.com/fraiseql/semis
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/fraiseql/semis/issues
|
|
9
|
+
Author-email: Lionel Hamayon <lionel@fraiseql.dev>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: database,fake-data,faker,fraiseql,postgresql,seeding,testing,uuid
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: faker>=24.0.0
|
|
25
|
+
Requires-Dist: fraiseql-confiture<2,>=1.27
|
|
26
|
+
Requires-Dist: psycopg[binary]>=3.1.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pre-commit>=3.6.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ty>=0.0.83; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# fraiseql-semis
|
|
38
|
+
|
|
39
|
+
**Reproducible seed and test data for PostgreSQL trinity-pattern schemas.**
|
|
40
|
+
|
|
41
|
+
semis generates rows: which rows exist, what each value looks like, and what UUID
|
|
42
|
+
identifies each one. It decides nothing about the schema those rows land in — every
|
|
43
|
+
question of the form *"what does the schema say?"* goes to
|
|
44
|
+
[confiture](https://github.com/fraiseql/confiture), which reads the DDL or the live
|
|
45
|
+
database, orders tables by their real foreign keys, says which columns a writer may supply
|
|
46
|
+
and what each value must respect, and writes, applies and validates the seed files.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What it does
|
|
51
|
+
|
|
52
|
+
- 🎯 **Semantic UUIDs** — every `id` encodes its table, scenario, version and sequence, so
|
|
53
|
+
a row in a dump tells you where it came from; the `identifier` slug carries the scenario
|
|
54
|
+
too, so two scenarios can share a database
|
|
55
|
+
- 🎲 **Faker, driven by facts** — a value satisfies the column that receives it: its type,
|
|
56
|
+
its enum values, its declared length, its NOT NULL
|
|
57
|
+
- 🔗 **Foreign keys that are real** — the parent table and column come from the schema's
|
|
58
|
+
actual foreign key, never from the spelling of `fk_continent`
|
|
59
|
+
- 📋 **YAML scenarios** — a run is a reviewable file, pinned to the schema it was written
|
|
60
|
+
against
|
|
61
|
+
- 🔁 **Reproducible** — same scenario, same seed, same schema, same bytes
|
|
62
|
+
- 🚫 **No invented keys** — PostgreSQL fills `pk_*`; semis never writes one
|
|
63
|
+
|
|
64
|
+
## What it does not do
|
|
65
|
+
|
|
66
|
+
semis reads no `information_schema`, sorts no tables, and writes no SQL by hand. All three
|
|
67
|
+
are confiture's, and going through it is why `VARCHAR(50)` gets fifty characters and why a
|
|
68
|
+
`REFERENCES` clause is read rather than guessed.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv add fraiseql-semis # brings fraiseql-confiture>=1.27,<2
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## The chain, end to end
|
|
79
|
+
|
|
80
|
+
This is the whole idea in one screen — generate, write, apply, read back:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
from fraiseql_semis import FakeDataGenerator, SchemaFacts, TableCodes, seeds
|
|
86
|
+
|
|
87
|
+
database_url = "postgresql:///myproject_dev"
|
|
88
|
+
codes = TableCodes({"catalog.tb_continent": 0x02030405})
|
|
89
|
+
|
|
90
|
+
# the project's DDL tree; SchemaFacts.from_env("development", table_codes=codes) reads
|
|
91
|
+
# a confiture environment's build, and .from_database(url, …) a live database
|
|
92
|
+
facts = SchemaFacts.from_source(Path("db/schema"), table_codes=codes)
|
|
93
|
+
gen = FakeDataGenerator(facts, scenario_id=0x5001, seed=42)
|
|
94
|
+
|
|
95
|
+
rows = gen.generate_rows(
|
|
96
|
+
"catalog.tb_continent",
|
|
97
|
+
count=3,
|
|
98
|
+
overrides={"identifier": lambda i: f"cont-{i + 1}", "name": lambda i: f"Continent {i + 1}"},
|
|
99
|
+
)
|
|
100
|
+
seed = seeds.write(
|
|
101
|
+
"db/seeds/010_tb_continent.sql", "catalog.tb_continent", rows, facts=facts, mode="read-back"
|
|
102
|
+
)
|
|
103
|
+
seeds.apply(database_url, [seed.path])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
What arrives in PostgreSQL — `psql` output from exactly this chain. The two key columns
|
|
107
|
+
are the point:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
pk_continent | id | identifier | name
|
|
111
|
+
--------------+--------------------------------------+------------+-------------
|
|
112
|
+
1 | 02030405-5001-0001-0000-000000000001 | cont-1 | Continent 1
|
|
113
|
+
2 | 02030405-5001-0001-0000-000000000002 | cont-2 | Continent 2
|
|
114
|
+
3 | 02030405-5001-0001-0000-000000000003 | cont-3 | Continent 3
|
|
115
|
+
(3 rows)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`pk_continent` is PostgreSQL's — semis could not write it if it wanted to:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
>>> write_copy_seed(path, "catalog.tb_continent", ["pk_continent", "id"], rows, model=model)
|
|
122
|
+
SeedError: PostgreSQL fills catalog.tb_continent.pk_continent (an identity, generated or
|
|
123
|
+
serial column): leave it out of the seed
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`id` is semis' — and it decodes:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
$ semis decode-uuid 02030405-5001-0001-0000-000000000001
|
|
130
|
+
table_code 0x02030405 catalog.tb_continent
|
|
131
|
+
scenario_id 0x5001 minimal_seed
|
|
132
|
+
version 1
|
|
133
|
+
sequence 1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## The two FK modes
|
|
139
|
+
|
|
140
|
+
A child needs its parent's integer `pk_*`, and semis cannot choose one. **A scenario
|
|
141
|
+
declares which of the two ways it closes that gap**; semis refuses to guess, because a
|
|
142
|
+
scenario that picks its mode by probing the database means two different things in CI and
|
|
143
|
+
on a laptop.
|
|
144
|
+
|
|
145
|
+
| | **prep-seed** | **read-back** |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| Rows are written into | the staging twin, `prep_seed.<table>` | the table itself |
|
|
148
|
+
| A child's FK carries | the parent's **UUID**, as `<fk>_id` | the parent's **integer**, learned after apply |
|
|
149
|
+
| Database needed to generate | no | yes |
|
|
150
|
+
| Translation done by | the project's `fn_resolve_*` functions | a join on the UUID semis wrote |
|
|
151
|
+
| Output is | byte-reproducible | reproducible except the FK integers, which are PostgreSQL's |
|
|
152
|
+
| Default writer | `INSERT` (confiture's prep-seed level 1 reads only INSERT) | `COPY` |
|
|
153
|
+
|
|
154
|
+
A prep-seed scenario names the catalog tables, whose foreign keys, constraints and pin
|
|
155
|
+
semis reads, and writes each into its **staging twin**: the same table name in the
|
|
156
|
+
project's staging schema, every foreign key a UUID named `<fk>_id` — the layout
|
|
157
|
+
confiture's prep-seed validation checks and a project's `fn_resolve_*` functions read.
|
|
158
|
+
A table without its twin is refused before a row is drawn.
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# scenarios/minimal_seed.yaml
|
|
162
|
+
scenario_id: 0x5001
|
|
163
|
+
name: minimal_seed
|
|
164
|
+
description: Seven continents and the countries on them
|
|
165
|
+
mode: prep-seed # required: prep-seed | read-back
|
|
166
|
+
locale: en_US
|
|
167
|
+
seed: 42
|
|
168
|
+
|
|
169
|
+
tables:
|
|
170
|
+
- name: catalog.tb_continent
|
|
171
|
+
count: 7
|
|
172
|
+
overrides:
|
|
173
|
+
name: [Africa, Antarctica, Asia, Europe, North America, Oceania, South America]
|
|
174
|
+
- name: catalog.tb_country
|
|
175
|
+
count: 50
|
|
176
|
+
trusts_trigger: [created_by] # a trigger fills it; do not refuse the row
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Every foreign key's parent is generated in the same run — a missing one is refused
|
|
180
|
+
before a row is drawn — unless the key is nullable and the scenario leaves it `NULL`:
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
- name: inventory.tb_item
|
|
184
|
+
count: 50
|
|
185
|
+
overrides: {fk_account: null, fk_order: null} # no account, no order
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
From Python, a scenario runs through `ScenarioManager`, which writes one seed file per
|
|
189
|
+
table and, beside them, `schema_pin.yaml` and the DDL it digested, `schema_pin.ddl`: the
|
|
190
|
+
block and the snapshot to copy beside the scenario so a replay against a moved schema is
|
|
191
|
+
refused.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from fraiseql_semis import ScenarioManager
|
|
195
|
+
|
|
196
|
+
manager = ScenarioManager(facts) # the SchemaFacts above
|
|
197
|
+
run = manager.execute(manager.load("scenarios/minimal_seed.yaml"), Path("db/seeds/prep"))
|
|
198
|
+
print(*run.notices) # scenario minimal_seed is unpinned: its schema is not checked
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
From the command line, a project names its schema and its table codes once, in
|
|
202
|
+
`semis.yaml`; a scenario names neither:
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
# semis.yaml
|
|
206
|
+
schema:
|
|
207
|
+
env: development # a confiture environment; or ddl: db/schema/ or
|
|
208
|
+
# database: {schemas: [catalog]}
|
|
209
|
+
scenarios: scenarios/
|
|
210
|
+
table_codes: # or a path to a YAML file of them
|
|
211
|
+
catalog.tb_continent: 0x02030405
|
|
212
|
+
catalog.tb_country: 0x03040506
|
|
213
|
+
providers: [i18n, organization, myproject.fake:PROVIDERS] # optional; see Providers
|
|
214
|
+
prep_seed:
|
|
215
|
+
prep_seed_schema: prep_seed # where prep-seed scenarios write; the default
|
|
216
|
+
schema_dir: db/0_schema # the tree holding the resolvers; default: a ddl: directory
|
|
217
|
+
catalog_schema: catalog # optional: a final table's fallback schema
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
semis seeds scenarios/minimal_seed.yaml -o db/seeds/prep # prep-seed: writes files, no database
|
|
222
|
+
semis seeds scenarios/minimal_seed.yaml --dry-run # every row checked, nothing written
|
|
223
|
+
semis apply scenarios/minimal_seed.yaml -o db/seeds/run # writes and applies, one transaction
|
|
224
|
+
semis generate scenarios/minimal_seed.yaml -o db/seeds/run # seeds for prep-seed, apply for read-back
|
|
225
|
+
semis validate scenarios/minimal_seed.yaml # schema and pin checked, no rows drawn
|
|
226
|
+
semis validate-seeds scenarios/minimal_seed.yaml # confiture's five prep-seed levels
|
|
227
|
+
semis validate-seeds --seeds db/seeds/prep --max-level 3 # a directory's seeds, files only
|
|
228
|
+
semis table catalog.tb_continent --count 3 --mode prep-seed --scenario-id 0x5001 -o out
|
|
229
|
+
semis list-scenarios
|
|
230
|
+
semis init-scenario demo --mode read-back # a template, with the next free id
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`apply` connects where confiture would: `--database-url`, then `CONFITURE_DATABASE_URL`,
|
|
234
|
+
then the `env:` file's `database_url`; the ambient `DATABASE_URL` alone is refused to a
|
|
235
|
+
command that writes. The whole run is one transaction, committed at the end, and
|
|
236
|
+
`apply --dry-run` runs it and rolls it back. A refusal exits 1, naming the table and the
|
|
237
|
+
column; a confiture error exits with confiture's own code and hint.
|
|
238
|
+
|
|
239
|
+
`validate-seeds` rehearses a prep-seed scenario into a temporary directory and has
|
|
240
|
+
confiture judge exactly its files; `--seeds DIR` judges a directory instead. Levels 1–3
|
|
241
|
+
read files. Levels 4–5 load the seeds into the staging twins and run the project's
|
|
242
|
+
resolvers on the database, found as `apply` finds it, inside a savepoint rolled back:
|
|
243
|
+
nothing is left behind. With no database URL it runs levels 1–3 and says so. Findings are
|
|
244
|
+
printed most severe first, each with its file and line and confiture's hint, and a
|
|
245
|
+
`CRITICAL` or `ERROR` exits 1.
|
|
246
|
+
|
|
247
|
+
`semis.yaml` loads into `fraiseql_semis.Project`, which a tool that drives semis from
|
|
248
|
+
Python builds directly, with no file.
|
|
249
|
+
|
|
250
|
+
### Providers
|
|
251
|
+
|
|
252
|
+
semis ships two provider libraries — `i18n` (ISO country, language and currency codes,
|
|
253
|
+
locales, time zones) and `organization` (company names, SIREN/SIRET and French VAT
|
|
254
|
+
numbers that check, job titles, contacts). A library named under
|
|
255
|
+
`providers:` draws every column its rules match by name and type — `lang VARCHAR(2)`
|
|
256
|
+
receives `fr`, a `MACADDR` column a MAC address — and never a value longer than the
|
|
257
|
+
column holds. A scenario names one provider for one column:
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
- name: catalog.tb_currency
|
|
261
|
+
count: 20
|
|
262
|
+
providers: {iso_code: i18n.currency_code, name: slogan}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`slogan` is the project's own: `myproject.fake:PROVIDERS` is a mapping of names to
|
|
266
|
+
functions taking the run's seeded `Faker` and the column's facts. It is imported; no
|
|
267
|
+
scenario string is ever evaluated.
|
|
268
|
+
|
|
269
|
+
### Your own provider library
|
|
270
|
+
|
|
271
|
+
A library another project can install — private to a company, or published — is a
|
|
272
|
+
`Library` registered under the `fraiseql_semis.providers` entry point:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
# acme_semis/__init__.py
|
|
276
|
+
from faker import Faker
|
|
277
|
+
|
|
278
|
+
from fraiseql_semis import TEXT_TYPES, ColumnFacts, Library, Rule
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def ticker(faker: Faker, _column: ColumnFacts) -> str:
|
|
282
|
+
return faker.lexify("????").upper()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
LIBRARY = Library(
|
|
286
|
+
"acme", # the name semis.yaml enables it by
|
|
287
|
+
{"ticker": ticker}, # a scenario names acme.ticker
|
|
288
|
+
rules=(Rule("ticker", names=frozenset({"ticker", "symbol"}), types=TEXT_TYPES),),
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```toml
|
|
293
|
+
# acme-semis's pyproject.toml
|
|
294
|
+
[project.entry-points."fraiseql_semis.providers"]
|
|
295
|
+
acme = "acme_semis:LIBRARY"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Installed beside semis (`uv add acme-semis`), it is enabled as a shipped one is —
|
|
299
|
+
`providers: [i18n, acme]` — and only then: installing a library enables nothing, and
|
|
300
|
+
only the libraries `semis.yaml` names are imported. A name two installed packages
|
|
301
|
+
claim, or a shipped name, is refused, naming the packages.
|
|
302
|
+
|
|
303
|
+
### Hierarchies
|
|
304
|
+
|
|
305
|
+
A table whose foreign key points at itself — a location inside a location — declares its
|
|
306
|
+
tree, and semis draws it breadth-first: the roots carry `NULL`, and every later row hangs
|
|
307
|
+
from an earlier one, `fan_out` children to a parent.
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
- name: catalog.tb_location
|
|
311
|
+
count: 20 # levels of 2, 6 and 12 rows
|
|
312
|
+
hierarchy:
|
|
313
|
+
parent: fk_parent_location # the nullable self-FK that builds the tree
|
|
314
|
+
roots: 2
|
|
315
|
+
fan_out: 3
|
|
316
|
+
path: path # read-back only: an ltree of pk_* labels, 1.5.12
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
In prep-seed a child carries its parent's UUID, like any child. In read-back each level is
|
|
320
|
+
written, applied and read back before the next is drawn, one seed file per level, and
|
|
321
|
+
`path:` is set from the keys PostgreSQL gave: the parent's path, a dot, the row's own
|
|
322
|
+
`pk_*`. A self-referencing table without a `hierarchy:` is refused.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Two things semis insists on
|
|
327
|
+
|
|
328
|
+
**A row is complete before it is written.** Confiture's writers do not check NOT NULL —
|
|
329
|
+
deliberately, because a trigger may fill a column and the schema model cannot know. So
|
|
330
|
+
semis checks: a column that is NOT NULL with no default must be in the row with a real
|
|
331
|
+
value, or the row is refused, naming the table, the column and the fact it failed. A
|
|
332
|
+
column a trigger really does fill is named in `trusts_trigger:`, per table, per column.
|
|
333
|
+
Without this the gap surfaces at apply time as a constraint violation against a file, with
|
|
334
|
+
no line and no generator in sight.
|
|
335
|
+
|
|
336
|
+
**A scenario knows the schema it was written against.** Each one records a digest of the
|
|
337
|
+
facts semis consumes — writable columns, their types, nullability, defaults, enums, checks,
|
|
338
|
+
FK targets — and a replay against a moved schema is refused, with `confiture.diff`
|
|
339
|
+
reporting what changed. Adding an index does not move the digest; adding `NOT NULL` to a
|
|
340
|
+
column semis writes does. `--no-pin` skips the check for one run and says so.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Documentation
|
|
345
|
+
|
|
346
|
+
| | |
|
|
347
|
+
|---|---|
|
|
348
|
+
| [docs/PRD.md](./docs/PRD.md) | What semis is for, and for whom |
|
|
349
|
+
| [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) | The boundaries, the two modes, determinism, the pin, the decision log |
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
uv sync --all-extras
|
|
355
|
+
uv run pytest tests/unit/ # no database needed
|
|
356
|
+
uv run ruff check . && uv run ruff format --check .
|
|
357
|
+
uv run ty check src/fraiseql_semis/
|
|
358
|
+
|
|
359
|
+
export SEMIS_TEST_DATABASE_URL=postgresql:///semis_test
|
|
360
|
+
uv run pytest tests/integration/
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Unit tests take their schema from a DDL string through `parse_schema`, so the generator,
|
|
364
|
+
the row contract and the UUID encoding are all testable without PostgreSQL. Tests under
|
|
365
|
+
`tests/contract/` pin the confiture behaviours semis relies on, so an upstream change
|
|
366
|
+
fails here with a name.
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT — see [LICENSE](./LICENSE).
|