xync-db 0.5.3__tar.gz → 0.5.5.dev9__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.
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/PKG-INFO +1 -1
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/CLAUDE.md +22 -3
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/0001_initial.py +211 -10
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/0002_triggers.py +140 -3
- xync_db-0.5.5.dev9/src/xync_db/migrations/models/0003_seed.py +59 -0
- xync_db-0.5.5.dev9/src/xync_db/migrations/models/seed/a_bin_seed.py +74 -0
- xync_db-0.5.5.dev9/src/xync_db/migrations/models/seed/a_country_seed.py +58 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/seed/a_tg_seed.py +2 -20
- xync_db-0.5.5.dev9/src/xync_db/migrations/models/seed/b_htx_seed.py +26 -0
- xync_db-0.5.5.dev9/src/xync_db/migrations/models/seed/countries.json +357 -0
- xync_db-0.5.5.dev9/src/xync_db/migrations/models/seed/ranges.csv +5806 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/models/__init__.py +191 -2
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/pm.py +1 -1
- xync_db-0.5.3/src/xync_db/migrations/models/0003_files_seed.py +0 -20
- xync_db-0.5.3/src/xync_db/migrations/models/0004_all_seed.py +0 -32
- xync_db-0.5.3/src/xync_db/migrations/models/0005_credex_del.py +0 -40
- xync_db-0.5.3/src/xync_db/migrations/models/0006_credex_orphan.py +0 -47
- xync_db-0.5.3/src/xync_db/migrations/models/0007_agent_auth_notify.py +0 -41
- xync_db-0.5.3/src/xync_db/migrations/models/0008_ad_exid_ex_uniq.py +0 -51
- xync_db-0.5.3/src/xync_db/migrations/models/0009_web3_tables.py +0 -145
- xync_db-0.5.3/src/xync_db/migrations/models/0010_updatestate_fix.py +0 -54
- xync_db-0.5.3/src/xync_db/migrations/models/0011_pair_scale.py +0 -45
- xync_db-0.5.3/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -111
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/.gitignore +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/README.md +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/pyproject.toml +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/__init__.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/config.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/enums.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/exceptions.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/graph.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/i18n.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/login_store.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/logo.png +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/__init__.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/__init__.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/seed/__init__.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/seed/c_net_seed.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/models/CLAUDE.md +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/py.typed +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/shared.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/__init__.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/__init__.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/actor.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/ad.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/common.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/cred.py +0 -0
- {xync_db-0.5.3 → xync_db-0.5.5.dev9}/src/xync_db/typs/db/order.py +0 -0
|
@@ -6,6 +6,19 @@ Native Tortoise ORM migrations. Config: `xync_service.config.TORTOISE_ORM`
|
|
|
6
6
|
CLI (from `packages/xync-db/`): `uv run tortoise makemigrations` / `uv run tortoise migrate`
|
|
7
7
|
(or `make makemigrations` / `make migrate` from the repo root).
|
|
8
8
|
|
|
9
|
+
> **Консолидировано в 3 миграции (D-037).** Прежние 0001-0012 слиты в:
|
|
10
|
+
> 1. **`0001_initial`** — ФИНАЛЬНАЯ схема всех таблиц (`ops.CreateModel`), включая всё, что раньше
|
|
11
|
+
> доливали поздние миграции: `pair.scale`+`rate` INT (было 0011), `country.ccn3` (было `code`, 0012),
|
|
12
|
+
> `updatestate` перестроен (0010), `net_addr.val` nullable + таблицы `coin_net`/`w3_send` (0009).
|
|
13
|
+
> 2. **`0002_triggers`** — ВСЕ триггеры (база + credex_del/credex_orphan/agent_upd-auth/ad_exid_ex_uniq/
|
|
14
|
+
> net_addr — были 0005-0009). `CREATE OR REPLACE` + порядок в `operations` дают финальную форму.
|
|
15
|
+
> 3. **`0003_seed`** — весь засев (files + actor/tg/country/htx/net) в порядке зависимостей.
|
|
16
|
+
>
|
|
17
|
+
> **Почему:** сид (0003) пишет через ORM (`set_pairs`→`pair.scale`) и должен идти по ПОЛНОЙ схеме;
|
|
18
|
+
> когда колонку добавляла миграция ПОСЛЕ сида, fresh-migrate падал «column … does not exist». Правило:
|
|
19
|
+
> **любое изменение схемы для засеваемой таблицы — в `0001`, а не в поздней миграции.**
|
|
20
|
+
> Разделы ниже описывают ИСТОРИЧЕСКОЕ содержимое (для контекста) — оно теперь внутри этих трёх файлов.
|
|
21
|
+
|
|
9
22
|
## models/0001_initial.py — Initial Schema
|
|
10
23
|
|
|
11
24
|
Creates all ~50 tables. Key tables and their relationships:
|
|
@@ -80,9 +93,15 @@ Loads PM logo files from `seed/xync_files.sql` if < 100 files exist. These are T
|
|
|
80
93
|
## models/0004_all_seed.py — Full Seed
|
|
81
94
|
|
|
82
95
|
Orchestrates the seed scripts in order:
|
|
83
|
-
1. `a_actor_seed` — Exchanges, currencies, coins, XyncPay PM, origin user (id=0)
|
|
84
|
-
2. `a_tg_seed` — Telegram DCs, app config, PM name mappings (`pm_rep`)
|
|
85
|
-
3. `
|
|
96
|
+
1. `a_actor_seed` — Exchanges, currencies (56 base), coins, XyncPay PM, origin user (id=0)
|
|
97
|
+
2. `a_tg_seed` — Telegram DCs, app config, PM name mappings (`pm_rep`)
|
|
98
|
+
3. `a_country_seed` — **all** countries + their currencies from the bundled `seed/countries.json`
|
|
99
|
+
(generated from mledoze/countries), single source, BEFORE the exchange seeders. Sets `ccn3`
|
|
100
|
+
(ISO 3166-1 numeric — the old `code`/phone column, renamed in 0012), clean `short` (cca2),
|
|
101
|
+
canonical `name`, real `cur` (creates the ~99 non-base `Cur`); `id` baked into the file so
|
|
102
|
+
existing countries keep theirs, meta-aggregates hold 201/202. Idempotent. (D-036)
|
|
103
|
+
4. `b_htx_seed` — HTX-specific: currencies (CurEx), coins, pairs (via `xync_client.ExClient`).
|
|
104
|
+
No longer seeds countries — those moved to `a_country_seed`.
|
|
86
105
|
|
|
87
106
|
> **PM-dependent seeding left the migration (D-017).** PMs aren't populated at migrate
|
|
88
107
|
> time (they come from `set_pms`), so anything keyed off them moved to `packages/xync-script`
|
|
@@ -35,6 +35,7 @@ from xync_db.enums import (
|
|
|
35
35
|
TaskType,
|
|
36
36
|
TransactionStatus,
|
|
37
37
|
UserStatus,
|
|
38
|
+
W3SendStatus,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
|
|
@@ -121,7 +122,7 @@ class Migration(migrations.Migration):
|
|
|
121
122
|
name="Country",
|
|
122
123
|
fields=[
|
|
123
124
|
("id", UInt1Field(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
124
|
-
("
|
|
125
|
+
("ccn3", UInt2Field(null=True)),
|
|
125
126
|
("short", fields.CharField(null=True, max_length=3)),
|
|
126
127
|
("name", fields.CharField(null=True, unique=True, max_length=63)),
|
|
127
128
|
(
|
|
@@ -424,7 +425,10 @@ class Migration(migrations.Migration):
|
|
|
424
425
|
on_delete=OnDelete.CASCADE,
|
|
425
426
|
),
|
|
426
427
|
),
|
|
427
|
-
|
|
428
|
+
# rate — 20-битная мантисса курса (пишется через best_scale); scale — 4-битная
|
|
429
|
+
# (−8..7) степень десятки, своя у каждой пары: real = rate / 10^scale (Pair.real).
|
|
430
|
+
("rate", fields.IntField(null=True)),
|
|
431
|
+
("scale", fields.SmallIntField(db_default=0)),
|
|
428
432
|
],
|
|
429
433
|
options={
|
|
430
434
|
"table": "pair",
|
|
@@ -503,6 +507,24 @@ class Migration(migrations.Migration):
|
|
|
503
507
|
options={"table": "pm_group", "app": "models", "pk_attr": "id"},
|
|
504
508
|
bases=["Model"],
|
|
505
509
|
),
|
|
510
|
+
# `bank` — раньше `Bank`/`Bin`/`Card` (D-038), т.к. на него ссылается `pm.bank_id`
|
|
511
|
+
ops.CreateModel(
|
|
512
|
+
name="Bank",
|
|
513
|
+
fields=[
|
|
514
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
515
|
+
("name", fields.CharField(unique=True, max_length=127)),
|
|
516
|
+
("url", fields.CharField(null=True, max_length=255)),
|
|
517
|
+
("phone", fields.CharField(null=True, max_length=31)),
|
|
518
|
+
("city", fields.CharField(null=True, max_length=63)),
|
|
519
|
+
],
|
|
520
|
+
options={
|
|
521
|
+
"table": "bank",
|
|
522
|
+
"app": "models",
|
|
523
|
+
"pk_attr": "id",
|
|
524
|
+
"table_description": "Card-issuing banks (enriched from binlist.net)",
|
|
525
|
+
},
|
|
526
|
+
bases=["Model"],
|
|
527
|
+
),
|
|
506
528
|
ops.CreateModel(
|
|
507
529
|
name="Pm",
|
|
508
530
|
fields=[
|
|
@@ -548,7 +570,18 @@ class Migration(migrations.Migration):
|
|
|
548
570
|
("alias", fields.CharField(null=True, max_length=63)),
|
|
549
571
|
("extra", fields.CharField(null=True, max_length=63)),
|
|
550
572
|
("ok", fields.BooleanField(db_default=True)),
|
|
551
|
-
(
|
|
573
|
+
(
|
|
574
|
+
"bank",
|
|
575
|
+
fields.ForeignKeyField(
|
|
576
|
+
"models.Bank",
|
|
577
|
+
source_field="bank_id",
|
|
578
|
+
null=True,
|
|
579
|
+
db_constraint=True,
|
|
580
|
+
to_field="id",
|
|
581
|
+
related_name="pms",
|
|
582
|
+
on_delete=OnDelete.CASCADE,
|
|
583
|
+
),
|
|
584
|
+
),
|
|
552
585
|
("qr", fields.BooleanField(db_default=False)),
|
|
553
586
|
("fee", UInt2Field(null=True)),
|
|
554
587
|
(
|
|
@@ -1758,13 +1791,100 @@ class Migration(migrations.Migration):
|
|
|
1758
1791
|
on_delete=OnDelete.CASCADE,
|
|
1759
1792
|
),
|
|
1760
1793
|
),
|
|
1761
|
-
|
|
1794
|
+
# val NULLABLE: адрес ещё не заведён (web3-хендшейк, триггеры net_addr_ins/upd в 0002)
|
|
1795
|
+
("val", fields.CharField(null=True, max_length=200)),
|
|
1762
1796
|
("memo", fields.CharField(null=True, max_length=54)),
|
|
1763
1797
|
("qr", fields.CharField(null=True, max_length=255)),
|
|
1764
1798
|
],
|
|
1765
1799
|
options={"table": "net_addr", "app": "models", "unique_together": (("net", "addr"),), "pk_attr": "id"},
|
|
1766
1800
|
bases=["Model"],
|
|
1767
1801
|
),
|
|
1802
|
+
ops.CreateModel(
|
|
1803
|
+
name="CoinNet",
|
|
1804
|
+
fields=[
|
|
1805
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
1806
|
+
(
|
|
1807
|
+
"coin",
|
|
1808
|
+
fields.ForeignKeyField(
|
|
1809
|
+
"models.Coin",
|
|
1810
|
+
source_field="coin_id",
|
|
1811
|
+
db_constraint=True,
|
|
1812
|
+
to_field="id",
|
|
1813
|
+
related_name="coin_nets",
|
|
1814
|
+
on_delete=OnDelete.CASCADE,
|
|
1815
|
+
),
|
|
1816
|
+
),
|
|
1817
|
+
(
|
|
1818
|
+
"net",
|
|
1819
|
+
fields.ForeignKeyField(
|
|
1820
|
+
"models.Net",
|
|
1821
|
+
source_field="net_id",
|
|
1822
|
+
db_constraint=True,
|
|
1823
|
+
to_field="id",
|
|
1824
|
+
related_name="coin_nets",
|
|
1825
|
+
on_delete=OnDelete.CASCADE,
|
|
1826
|
+
),
|
|
1827
|
+
),
|
|
1828
|
+
("contract", fields.CharField(null=True, max_length=128)),
|
|
1829
|
+
("decimals", UInt1Field()),
|
|
1830
|
+
],
|
|
1831
|
+
options={
|
|
1832
|
+
"table": "coin_net",
|
|
1833
|
+
"app": "models",
|
|
1834
|
+
"unique_together": (("coin", "net"),),
|
|
1835
|
+
"pk_attr": "id",
|
|
1836
|
+
"table_description": "Coin presence per web3 network",
|
|
1837
|
+
},
|
|
1838
|
+
bases=["Model"],
|
|
1839
|
+
),
|
|
1840
|
+
ops.CreateModel(
|
|
1841
|
+
name="W3Send",
|
|
1842
|
+
fields=[
|
|
1843
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
1844
|
+
("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
|
|
1845
|
+
("updated_at", DatetimeSecField(db_default=Now(), auto_now=True, auto_now_add=False)),
|
|
1846
|
+
(
|
|
1847
|
+
"sender",
|
|
1848
|
+
fields.ForeignKeyField(
|
|
1849
|
+
"models.NetAddr",
|
|
1850
|
+
source_field="sender_id",
|
|
1851
|
+
db_constraint=True,
|
|
1852
|
+
to_field="id",
|
|
1853
|
+
related_name="w3_sends_out",
|
|
1854
|
+
on_delete=OnDelete.CASCADE,
|
|
1855
|
+
),
|
|
1856
|
+
),
|
|
1857
|
+
(
|
|
1858
|
+
"receiver",
|
|
1859
|
+
fields.ForeignKeyField(
|
|
1860
|
+
"models.NetAddr",
|
|
1861
|
+
source_field="receiver_id",
|
|
1862
|
+
db_constraint=True,
|
|
1863
|
+
to_field="id",
|
|
1864
|
+
related_name="w3_sends_in",
|
|
1865
|
+
on_delete=OnDelete.CASCADE,
|
|
1866
|
+
),
|
|
1867
|
+
),
|
|
1868
|
+
("amount", UInt8Field()),
|
|
1869
|
+
(
|
|
1870
|
+
"status",
|
|
1871
|
+
fields.IntEnumField(
|
|
1872
|
+
description="pending: 0\naccepted: 1\ndeclined: 2\ndone: 3",
|
|
1873
|
+
db_default=W3SendStatus.pending,
|
|
1874
|
+
enum_type=W3SendStatus,
|
|
1875
|
+
generated=False,
|
|
1876
|
+
),
|
|
1877
|
+
),
|
|
1878
|
+
("tx_hash", fields.CharField(null=True, max_length=128)),
|
|
1879
|
+
],
|
|
1880
|
+
options={
|
|
1881
|
+
"table": "w3_send",
|
|
1882
|
+
"app": "models",
|
|
1883
|
+
"pk_attr": "id",
|
|
1884
|
+
"table_description": "Outbound web3 send (NetAddr → NetAddr)",
|
|
1885
|
+
},
|
|
1886
|
+
bases=["Model", "TsTrait"],
|
|
1887
|
+
),
|
|
1768
1888
|
ops.CreateModel(
|
|
1769
1889
|
name="PayReq",
|
|
1770
1890
|
fields=[
|
|
@@ -2427,7 +2547,7 @@ class Migration(migrations.Migration):
|
|
|
2427
2547
|
ops.CreateModel(
|
|
2428
2548
|
name="UpdateState",
|
|
2429
2549
|
fields=[
|
|
2430
|
-
("id",
|
|
2550
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
2431
2551
|
(
|
|
2432
2552
|
"session",
|
|
2433
2553
|
fields.ForeignKeyField(
|
|
@@ -2439,12 +2559,21 @@ class Migration(migrations.Migration):
|
|
|
2439
2559
|
on_delete=OnDelete.CASCADE,
|
|
2440
2560
|
),
|
|
2441
2561
|
),
|
|
2442
|
-
(
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2562
|
+
(
|
|
2563
|
+
"chat_id",
|
|
2564
|
+
fields.BigIntField(db_default=0, description="pyrogram get_channel_id(-100..), 0 = common state"),
|
|
2565
|
+
),
|
|
2566
|
+
("pts", UInt4Field()),
|
|
2567
|
+
("qts", UInt4Field(null=True)),
|
|
2568
|
+
("date", UInt4Field()),
|
|
2569
|
+
("seq", UInt4Field(null=True)),
|
|
2446
2570
|
],
|
|
2447
|
-
options={
|
|
2571
|
+
options={
|
|
2572
|
+
"table": "updatestate",
|
|
2573
|
+
"app": "models",
|
|
2574
|
+
"unique_together": (("session", "chat_id"),),
|
|
2575
|
+
"pk_attr": "id",
|
|
2576
|
+
},
|
|
2448
2577
|
bases=["Model"],
|
|
2449
2578
|
),
|
|
2450
2579
|
ops.CreateModel(
|
|
@@ -2477,6 +2606,78 @@ class Migration(migrations.Migration):
|
|
|
2477
2606
|
options={"table": "vpn", "app": "models", "pk_attr": "id", "table_description": "VPNs"},
|
|
2478
2607
|
bases=["Model"],
|
|
2479
2608
|
),
|
|
2609
|
+
# ── Cards / BINs (D-038; засев — `0003_seed` → a_bin_seed из binlist/data; `Bank` создан выше — на него ссылается `pm`) ──
|
|
2610
|
+
ops.CreateModel(
|
|
2611
|
+
name="Bin",
|
|
2612
|
+
fields=[
|
|
2613
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
2614
|
+
("prefix", UInt4Field(unique=True)),
|
|
2615
|
+
("length", UInt1Field(db_default=8)),
|
|
2616
|
+
("card_length", UInt1Field(db_default=16)),
|
|
2617
|
+
("luhn", fields.BooleanField(db_default=True)),
|
|
2618
|
+
("scheme", fields.CharField(null=True, max_length=31)),
|
|
2619
|
+
("typ", fields.CharField(null=True, max_length=15)),
|
|
2620
|
+
("brand", fields.CharField(null=True, max_length=63)),
|
|
2621
|
+
("prepaid", fields.BooleanField(null=True)),
|
|
2622
|
+
(
|
|
2623
|
+
"country",
|
|
2624
|
+
fields.ForeignKeyField(
|
|
2625
|
+
"models.Country",
|
|
2626
|
+
source_field="country_id",
|
|
2627
|
+
null=True,
|
|
2628
|
+
db_constraint=True,
|
|
2629
|
+
to_field="id",
|
|
2630
|
+
related_name="bins",
|
|
2631
|
+
on_delete=OnDelete.CASCADE,
|
|
2632
|
+
),
|
|
2633
|
+
),
|
|
2634
|
+
(
|
|
2635
|
+
"bank",
|
|
2636
|
+
fields.ForeignKeyField(
|
|
2637
|
+
"models.Bank",
|
|
2638
|
+
source_field="bank_id",
|
|
2639
|
+
null=True,
|
|
2640
|
+
db_constraint=True,
|
|
2641
|
+
to_field="id",
|
|
2642
|
+
related_name="bins",
|
|
2643
|
+
on_delete=OnDelete.CASCADE,
|
|
2644
|
+
),
|
|
2645
|
+
),
|
|
2646
|
+
],
|
|
2647
|
+
options={
|
|
2648
|
+
"table": "bin",
|
|
2649
|
+
"app": "models",
|
|
2650
|
+
"pk_attr": "id",
|
|
2651
|
+
"table_description": "Card BIN ranges (binlist.net)",
|
|
2652
|
+
},
|
|
2653
|
+
bases=["Model"],
|
|
2654
|
+
),
|
|
2655
|
+
ops.CreateModel(
|
|
2656
|
+
name="Card",
|
|
2657
|
+
fields=[
|
|
2658
|
+
("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
|
|
2659
|
+
(
|
|
2660
|
+
"bin",
|
|
2661
|
+
fields.ForeignKeyField(
|
|
2662
|
+
"models.Bin",
|
|
2663
|
+
source_field="bin_id",
|
|
2664
|
+
db_constraint=True,
|
|
2665
|
+
to_field="id",
|
|
2666
|
+
related_name="cards",
|
|
2667
|
+
on_delete=OnDelete.CASCADE,
|
|
2668
|
+
),
|
|
2669
|
+
),
|
|
2670
|
+
("num", UInt4Field()),
|
|
2671
|
+
],
|
|
2672
|
+
options={
|
|
2673
|
+
"table": "card",
|
|
2674
|
+
"app": "models",
|
|
2675
|
+
"unique_together": (("bin", "num"),),
|
|
2676
|
+
"pk_attr": "id",
|
|
2677
|
+
"table_description": "Cards: number stored without BIN and without the Luhn check digit",
|
|
2678
|
+
},
|
|
2679
|
+
bases=["Model"],
|
|
2680
|
+
),
|
|
2480
2681
|
ops.RunSQL(
|
|
2481
2682
|
"ALTER TABLE pm DROP CONSTRAINT uid_pm_norm_83aa05;"
|
|
2482
2683
|
"ALTER TABLE pm ADD CONSTRAINT uid_pm_norm_83aa05 UNIQUE NULLS NOT DISTINCT (norm, country_id);"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
"""PG triggers migration
|
|
2
|
-
|
|
1
|
+
"""PG triggers migration — ВСЕ триггеры БД в одном месте.
|
|
2
|
+
|
|
3
|
+
Определяют NOTIFY-триггеры, которые слушает pg-listener service-api. Раньше часть жила отдельными
|
|
4
|
+
миграциями (0005-0009); слиты сюда. `CREATE OR REPLACE` + порядок в `operations` дают финальную форму.
|
|
3
5
|
|
|
4
6
|
Channels emitted:
|
|
5
7
|
after_orders_new -- new order, no hot_id
|
|
@@ -7,10 +9,16 @@ Channels emitted:
|
|
|
7
9
|
hot_order_upd_status -- hot order status change
|
|
8
10
|
after_orders_upd_status -- non-hot order status change
|
|
9
11
|
agents_upd_status -- agent status toggle
|
|
12
|
+
agents_upd_auth -- agent web-session (cookies/headers) changed (0007)
|
|
10
13
|
actor_person_needs_def_user -- new actor needs person assignment
|
|
11
14
|
after_users_new -- new user (also creates person + xync-actor)
|
|
15
|
+
tx_verified -- transaction reached verified
|
|
16
|
+
ads_upd / mads_upd -- ad row changed (diff payload)
|
|
17
|
+
creds_new/creds_del/creds_upd, credexs_del (0005), credexs_orphan (0006) -- cred/credex lifecycle
|
|
18
|
+
w3addr_wanted / w3addr_filled -- web3 address handshake (0009)
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
Guards (RAISE, без NOTIFY): tx_before (баланс/иммутабельность/монотонность), tx_no_delete,
|
|
21
|
+
ad_exid_ex_uniq (0008, exid уникален per-exchange). Utility: _user_balance(uid, cid), _tx_pack(...).
|
|
14
22
|
|
|
15
23
|
`actor_new` trigger: link the auto-created anonymous person + stamp its `note` (D-035).
|
|
16
24
|
|
|
@@ -263,6 +271,129 @@ $cred_del_trg$ LANGUAGE plpgsql;
|
|
|
263
271
|
CREATE OR REPLACE TRIGGER cred_del BEFORE DELETE ON "cred" FOR EACH ROW EXECUTE FUNCTION cred_del();
|
|
264
272
|
"""
|
|
265
273
|
|
|
274
|
+
# ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
275
|
+
# Дополнительные триггеры (были отдельными миграциями 0005-0009; здесь слиты). Все `CREATE OR REPLACE`,
|
|
276
|
+
# поэтому порядок в `operations` гарантирует финальную форму: `credex_del` переопределяет дефолтный
|
|
277
|
+
# `plsql("credex")` DEL, а `agent_upd` — базовый выше.
|
|
278
|
+
# ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
# Per-CredEx delete → `credexs_del` (0005). Юзер отвязывает ОДИН кред от ОДНОЙ биржи. Дефолтный
|
|
281
|
+
# `plsql("credex")` DEL нёс лишь OLD.id — бесполезно после каскада. Шлём [person_id, ex_id, exid]
|
|
282
|
+
# (person через ещё живой родительский `cred`). ПРОПУСКАЕМ notify если родитель-cred уже удалён:
|
|
283
|
+
# это каскад Cred → BEFORE-DELETE `cred_del()` уже снял каждый credex в `creds_del` (иначе двойное
|
|
284
|
+
# удаление на бирже). Одиночное удаление credex оставляет cred живым → notify.
|
|
285
|
+
CREDEX_DEL_SQL = """
|
|
286
|
+
CREATE OR REPLACE FUNCTION credex_del() returns trigger as $credex_del_trg$
|
|
287
|
+
DECLARE
|
|
288
|
+
pid INT;
|
|
289
|
+
BEGIN
|
|
290
|
+
-- person via the parent cred; NULL ⇒ cred already cascade-deleted ⇒ cred_del() handled it ⇒ skip
|
|
291
|
+
SELECT person_id INTO pid FROM cred WHERE id = OLD.cred_id;
|
|
292
|
+
IF pid IS NOT NULL THEN
|
|
293
|
+
PERFORM pg_notify('credexs_del', jsonb_build_array(pid, OLD.ex_id, OLD.exid)::varchar);
|
|
294
|
+
END IF;
|
|
295
|
+
RETURN NULL;
|
|
296
|
+
END
|
|
297
|
+
$credex_del_trg$ LANGUAGE plpgsql;
|
|
298
|
+
CREATE OR REPLACE TRIGGER credex_del AFTER DELETE ON "credex" FOR EACH ROW EXECUTE FUNCTION credex_del();
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
# Per-CredEx exid-overwrite → `credexs_orphan` (0006). `CredEx.upsert` ключуется по (cred, ex) и
|
|
302
|
+
# перезаписывает `exid`. Перезапись РЕАЛЬНОГО (не 0) exid другим значением осиротит прежний кред на
|
|
303
|
+
# бирже (БД его больше не трекает) — не утекаем молча: BEFORE-UPDATE шлёт [person_id, ex_id, OLD.exid,
|
|
304
|
+
# OLD.cred_id], чтобы service-api спросил владельца, удалить ли устаревший (mirror `is_you`).
|
|
305
|
+
# Срабатывает только на настоящую перезапись real→другой: OLD.exid<>0 (стаб 0→real — в `credex_new`) и
|
|
306
|
+
# NEW.exid<>OLD.exid (пропуск no-op переписи). Перерегистрация идёт сразу (RETURN NEW); чистанка — async.
|
|
307
|
+
CREDEX_ORPHAN_SQL = """
|
|
308
|
+
CREATE OR REPLACE FUNCTION credex_orphan() returns trigger as $credex_orphan_trg$
|
|
309
|
+
DECLARE
|
|
310
|
+
pid INT;
|
|
311
|
+
BEGIN
|
|
312
|
+
IF OLD.exid <> 0 AND NEW.exid <> OLD.exid THEN
|
|
313
|
+
SELECT person_id INTO pid FROM cred WHERE id = OLD.cred_id;
|
|
314
|
+
IF pid IS NOT NULL THEN
|
|
315
|
+
PERFORM pg_notify('credexs_orphan', jsonb_build_array(pid, OLD.ex_id, OLD.exid, OLD.cred_id)::varchar);
|
|
316
|
+
END IF;
|
|
317
|
+
END IF;
|
|
318
|
+
RETURN NEW;
|
|
319
|
+
END
|
|
320
|
+
$credex_orphan_trg$ LANGUAGE plpgsql;
|
|
321
|
+
CREATE OR REPLACE TRIGGER credex_orphan BEFORE UPDATE ON "credex" FOR EACH ROW EXECUTE FUNCTION credex_orphan();
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# Agent.auth change → `agents_upd_auth` (0007). ПЕРЕОПРЕДЕЛЯЕТ `agent_upd` выше: второй, независимый
|
|
325
|
+
# branch шлёт `agents_upd_auth` ТОЛЬКО при реальной смене auth (`OLD.auth IS DISTINCT FROM NEW.auth`) —
|
|
326
|
+
# service-api патчит куки/хедеры в ЖИВУЮ http-сессию агента (`BaseAgentClient.reload_auth`) без рестарта.
|
|
327
|
+
# Независим от status-branch (status и auth меняются вместе или порознь).
|
|
328
|
+
AGENT_AUTH_SQL = """
|
|
329
|
+
CREATE OR REPLACE FUNCTION agent_upd() returns trigger as $agent_upd_trg$
|
|
330
|
+
BEGIN
|
|
331
|
+
IF OLD.status != NEW.status THEN
|
|
332
|
+
PERFORM pg_notify('agents_upd_status', jsonb_build_array(row_to_json(NEW), OLD.status)::varchar);
|
|
333
|
+
END IF;
|
|
334
|
+
IF OLD.auth IS DISTINCT FROM NEW.auth THEN
|
|
335
|
+
PERFORM pg_notify('agents_upd_auth', row_to_json(NEW)::varchar);
|
|
336
|
+
END IF;
|
|
337
|
+
RETURN NULL;
|
|
338
|
+
END
|
|
339
|
+
$agent_upd_trg$ LANGUAGE plpgsql;
|
|
340
|
+
CREATE OR REPLACE TRIGGER agent_upd AFTER UPDATE ON "agent" FOR EACH ROW EXECUTE FUNCTION agent_upd();
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
# Ad.exid uniqueness per-EXCHANGE, не per-maker (0008). `unique_together(exid, maker)` не ловит РАЗНОГО
|
|
344
|
+
# maker на ТОЙ ЖЕ бирже с чужим exid (биржа выдаёт ad-id глобально по аккаунтам). Tortoise не выразит
|
|
345
|
+
# uniq через join (`maker`→`actor.ex_id` не колонка `ad`) → BEFORE INSERT/UPDATE RAISE на конфликт по
|
|
346
|
+
# (exid≠0, тот же actor.ex_id). Падает громко на записи (как fail-loud в `Ex.e2x_ad`).
|
|
347
|
+
AD_EXID_EX_UNIQ_SQL = """
|
|
348
|
+
CREATE OR REPLACE FUNCTION ad_exid_ex_uniq() returns trigger as $ad_exid_ex_uniq_trg$
|
|
349
|
+
DECLARE
|
|
350
|
+
new_ex_id INT;
|
|
351
|
+
conflict_id INT;
|
|
352
|
+
conflict_maker_id INT;
|
|
353
|
+
BEGIN
|
|
354
|
+
IF NEW.exid = 0 THEN
|
|
355
|
+
RETURN NEW;
|
|
356
|
+
END IF;
|
|
357
|
+
SELECT ex_id INTO new_ex_id FROM actor WHERE id = NEW.maker_id;
|
|
358
|
+
SELECT a.id, a.maker_id INTO conflict_id, conflict_maker_id
|
|
359
|
+
FROM ad a JOIN actor act ON act.id = a.maker_id
|
|
360
|
+
WHERE a.exid = NEW.exid AND act.ex_id = new_ex_id AND a.id != NEW.id
|
|
361
|
+
LIMIT 1;
|
|
362
|
+
IF conflict_id IS NOT NULL THEN
|
|
363
|
+
RAISE EXCEPTION 'ad.exid=% already used by ad#% (maker#%) on ex_id=% — exid must be unique per exchange, rejected write for maker#%',
|
|
364
|
+
NEW.exid, conflict_id, conflict_maker_id, new_ex_id, NEW.maker_id;
|
|
365
|
+
END IF;
|
|
366
|
+
RETURN NEW;
|
|
367
|
+
END
|
|
368
|
+
$ad_exid_ex_uniq_trg$ LANGUAGE plpgsql;
|
|
369
|
+
CREATE OR REPLACE TRIGGER ad_exid_ex_uniq BEFORE INSERT OR UPDATE ON "ad" FOR EACH ROW EXECUTE FUNCTION ad_exid_ex_uniq();
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
# web3-хендшейк адресов (0009): net_addr_ins (вставка пустого val → зовём получателя завести адрес,
|
|
373
|
+
# канал `w3addr_wanted`) и net_addr_upd (val NULL→real → сообщаем отправителям pending-W3Send, канал
|
|
374
|
+
# `w3addr_filled`). (`ALTER net_addr.val DROP NOT NULL` уехал в 0001 — теперь val создаётся nullable.)
|
|
375
|
+
NET_ADDR_TRIGGERS_SQL = """
|
|
376
|
+
CREATE OR REPLACE FUNCTION net_addr_ins() RETURNS trigger AS $net_addr_ins$
|
|
377
|
+
BEGIN
|
|
378
|
+
IF NEW.val IS NULL THEN
|
|
379
|
+
PERFORM pg_notify('w3addr_wanted', row_to_json(NEW)::varchar);
|
|
380
|
+
END IF;
|
|
381
|
+
RETURN NULL;
|
|
382
|
+
END
|
|
383
|
+
$net_addr_ins$ LANGUAGE plpgsql;
|
|
384
|
+
CREATE OR REPLACE TRIGGER net_addr_ins AFTER INSERT ON "net_addr" FOR EACH ROW EXECUTE FUNCTION net_addr_ins();
|
|
385
|
+
|
|
386
|
+
CREATE OR REPLACE FUNCTION net_addr_upd() RETURNS trigger AS $net_addr_upd$
|
|
387
|
+
BEGIN
|
|
388
|
+
IF OLD.val IS NULL AND NEW.val IS NOT NULL THEN
|
|
389
|
+
PERFORM pg_notify('w3addr_filled', row_to_json(NEW)::varchar);
|
|
390
|
+
END IF;
|
|
391
|
+
RETURN NULL;
|
|
392
|
+
END
|
|
393
|
+
$net_addr_upd$ LANGUAGE plpgsql;
|
|
394
|
+
CREATE OR REPLACE TRIGGER net_addr_upd AFTER UPDATE ON "net_addr" FOR EACH ROW EXECUTE FUNCTION net_addr_upd();
|
|
395
|
+
"""
|
|
396
|
+
|
|
266
397
|
|
|
267
398
|
class Migration(BaseMigration):
|
|
268
399
|
dependencies: ClassVar = [("models", "0001_initial")]
|
|
@@ -275,4 +406,10 @@ class Migration(BaseMigration):
|
|
|
275
406
|
+ plsql("my_ad")
|
|
276
407
|
+ plsql("credex")
|
|
277
408
|
),
|
|
409
|
+
# порядок важен: credex_del/agent_upd переопределяют созданные выше (plsql + база)
|
|
410
|
+
RunSQL(CREDEX_DEL_SQL),
|
|
411
|
+
RunSQL(CREDEX_ORPHAN_SQL),
|
|
412
|
+
RunSQL(AGENT_AUTH_SQL),
|
|
413
|
+
RunSQL(AD_EXID_EX_UNIQ_SQL),
|
|
414
|
+
RunSQL(NET_ADDR_TRIGGERS_SQL),
|
|
278
415
|
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Полный засев БД одной миграцией — файлы (PM-логотипы) + справочники/сущности в нужном порядке.
|
|
2
|
+
|
|
3
|
+
Слияние прежних `0003_files_seed` и `0004_all_seed`. Идёт ПОСЛЕ триггеров (0002) и по ПОЛНОЙ схеме
|
|
4
|
+
(0001), поэтому сидеры, пишущие через ORM (напр. `set_pairs` → `pair.scale`), не спотыкаются о
|
|
5
|
+
недостающие колонки (раньше `pair.scale`/`country.ccn3` добавлялись миграциями ПОСЛЕ засева).
|
|
6
|
+
|
|
7
|
+
Порядок (зависимости):
|
|
8
|
+
1. files — PM-логотипы из `seed/xync_files.sql` (если файлов < 100)
|
|
9
|
+
2. a_actor_seed — биржи, 56 базовых валют, монеты, XyncPay PM, origin user (id=0)
|
|
10
|
+
3. a_tg_seed — Telegram DC/App, нормализация имён PM (`pm_rep`)
|
|
11
|
+
4. a_country_seed — ВСЕ страны + их валюты из mledoze (`seed/countries.json`), ДО биржевых
|
|
12
|
+
5. a_bin_seed — BIN-диапазоны + банки из binlist/data (`seed/ranges.csv`), ПОСЛЕ стран (линк по alpha-2)
|
|
13
|
+
6. b_htx_seed — HTX-специфика: валюты (CurEx), монеты, пары
|
|
14
|
+
7. c_net_seed — web3-сети (Net) + недостающие нативные монеты
|
|
15
|
+
|
|
16
|
+
NB (D-017): PM-зависимый засев (`set_pms` + `refine_pms`) живёт в `packages/xync-script`, НЕ здесь —
|
|
17
|
+
он тянет `pyro_client` (FileClient), а миграции не должны импортировать `pyro_client`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from os.path import dirname
|
|
21
|
+
from typing import ClassVar
|
|
22
|
+
|
|
23
|
+
from tortoise.migrations.migration import Migration as BaseMigration
|
|
24
|
+
from tortoise.migrations.operations import RunPython
|
|
25
|
+
|
|
26
|
+
from xync_db.migrations.models.seed import (
|
|
27
|
+
a_actor_seed,
|
|
28
|
+
a_bin_seed,
|
|
29
|
+
a_country_seed,
|
|
30
|
+
a_tg_seed,
|
|
31
|
+
b_htx_seed,
|
|
32
|
+
c_net_seed,
|
|
33
|
+
)
|
|
34
|
+
from xync_db.models import File
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def seed(apps, schema_editor):
|
|
38
|
+
db = schema_editor.client
|
|
39
|
+
# 1) файлы PM-логотипов
|
|
40
|
+
if await File.all().count() < 100:
|
|
41
|
+
with open(dirname(__file__) + "/seed/xync_files.sql") as f:
|
|
42
|
+
await schema_editor._run_sql(f.read())
|
|
43
|
+
# 2) справочники и сущности в порядке зависимостей
|
|
44
|
+
sql = await a_actor_seed.actor_seed(db)
|
|
45
|
+
sql += await a_tg_seed.tg_seed(db)
|
|
46
|
+
sql += await a_country_seed.country_seed(db) # страны+валюты из mledoze, ДО биржевых сидеров
|
|
47
|
+
sql += await a_bin_seed.bin_seed(db) # BIN-диапазоны+банки из binlist/data, ПОСЛЕ стран (линк по alpha-2)
|
|
48
|
+
sql += await b_htx_seed.htx_seed(db)
|
|
49
|
+
sql += await c_net_seed.net_seed(db)
|
|
50
|
+
if sql.strip():
|
|
51
|
+
await schema_editor._run_sql(sql)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Migration(BaseMigration):
|
|
55
|
+
atomic = False
|
|
56
|
+
dependencies: ClassVar = [("models", "0002_triggers")]
|
|
57
|
+
operations: ClassVar = [
|
|
58
|
+
RunPython(seed),
|
|
59
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Сидер BIN-диапазонов и банков-эмитентов из binlist/data — ПОСЛЕ стран (`a_country_seed`).
|
|
2
|
+
|
|
3
|
+
Данные — статический `ranges.csv` рядом (снимок https://github.com/binlist/data, `ranges.csv`):
|
|
4
|
+
колонки `iin_start,iin_end,number_length,number_luhn,scheme,brand,type,prepaid,country,bank_name,
|
|
5
|
+
bank_logo,bank_url,bank_phone,bank_city`. `iin_start` — BIN-префикс (6 ИЛИ 8 цифр), `iin_end` —
|
|
6
|
+
верх диапазона (пусто = одиночный). Диапазоны маленькие (макс. span 18), поэтому РАЗВОРАЧИВАЕМ их в
|
|
7
|
+
отдельные префиксы — модель `Bin` остаётся префиксной (`Bin.for_card` ищет 6- и 8-значный префикс,
|
|
8
|
+
`Card.from_number` режет тело по длине найденного BIN). См. D-038.
|
|
9
|
+
|
|
10
|
+
Идём после стран, т.к. `Bin.country` линкуется на уже засеянные `Country` по alpha-2 (`short`).
|
|
11
|
+
Банки дедуплицируем по `name` (первый вариант деталей выигрывает). Идемпотентно: guard по count +
|
|
12
|
+
`bulk_create(ignore_conflicts=True)`. Только линкуем существующие страны — новые не заводим
|
|
13
|
+
(`Country` требует `cur_id`), незнакомый alpha-2 → `country_id=NULL`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import csv
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from tortoise import BaseDBAsyncClient
|
|
22
|
+
|
|
23
|
+
from xync_db.models import Bank, Bin, Country
|
|
24
|
+
|
|
25
|
+
DATA = Path(__file__).with_name("ranges.csv")
|
|
26
|
+
_SEEDED_ENOUGH = 5000 # если BIN-ов уже столько — считаем засеянным, полный проход не гоняем
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def bin_seed(_: BaseDBAsyncClient) -> str:
|
|
30
|
+
if await Bin.all().count() >= _SEEDED_ENOUGH:
|
|
31
|
+
return "select 1;" # уже засеяно
|
|
32
|
+
|
|
33
|
+
rows = list(csv.DictReader(DATA.read_text(encoding="utf-8").splitlines()))
|
|
34
|
+
|
|
35
|
+
# 1) банки — по уникальному name (первый вариант деталей выигрывает)
|
|
36
|
+
banks: dict[str, dict] = {}
|
|
37
|
+
for r in rows:
|
|
38
|
+
if (name := r["bank_name"].strip()) and name not in banks:
|
|
39
|
+
banks[name] = {
|
|
40
|
+
"url": r["bank_url"].strip() or None,
|
|
41
|
+
"phone": r["bank_phone"].strip() or None,
|
|
42
|
+
"city": r["bank_city"].strip() or None,
|
|
43
|
+
}
|
|
44
|
+
await Bank.bulk_create([Bank(name=n, **d) for n, d in banks.items()], ignore_conflicts=True)
|
|
45
|
+
bank_id: dict[str, int] = {b.name: b.id for b in await Bank.all()}
|
|
46
|
+
country_id: dict[str, int] = {c.short: c.id for c in await Country.all() if c.short}
|
|
47
|
+
|
|
48
|
+
# 2) BIN-ы — развернуть диапазоны в отдельные префиксы
|
|
49
|
+
bins: list[Bin] = []
|
|
50
|
+
for r in rows:
|
|
51
|
+
start = int(r["iin_start"])
|
|
52
|
+
end = int(r["iin_end"]) if r["iin_end"].strip() else start
|
|
53
|
+
length = len(r["iin_start"])
|
|
54
|
+
card_length = int(r["number_length"]) if r["number_length"].strip() else 16
|
|
55
|
+
prepaid = True if r["prepaid"].strip().lower() == "y" else None # 'y' | пусто
|
|
56
|
+
common = {
|
|
57
|
+
"length": length,
|
|
58
|
+
"card_length": card_length,
|
|
59
|
+
"luhn": r["number_luhn"].strip().lower() != "false", # в датасете всегда пусто → True
|
|
60
|
+
"scheme": r["scheme"].strip() or None,
|
|
61
|
+
"typ": r["type"].strip() or None,
|
|
62
|
+
"brand": r["brand"].strip() or None,
|
|
63
|
+
"prepaid": prepaid,
|
|
64
|
+
"country_id": country_id.get(r["country"].strip()),
|
|
65
|
+
"bank_id": bank_id.get(r["bank_name"].strip()),
|
|
66
|
+
}
|
|
67
|
+
bins.extend(Bin(prefix=pfx, **common) for pfx in range(start, end + 1))
|
|
68
|
+
await Bin.bulk_create(bins, ignore_conflicts=True, batch_size=1000)
|
|
69
|
+
|
|
70
|
+
return "select 1;"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def downgrade(_: BaseDBAsyncClient) -> str:
|
|
74
|
+
return ""
|