langflow-base-nightly 0.5.0.dev33__py3-none-any.whl → 0.5.0.dev35__py3-none-any.whl
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.
- langflow/alembic/versions/1cb603706752_modify_uniqueness_constraint_on_file_.py +279 -0
- langflow/api/v1/endpoints.py +1 -1
- langflow/base/composio/composio_base.py +1092 -126
- langflow/components/agents/mcp_component.py +21 -4
- langflow/components/composio/__init__.py +24 -0
- langflow/components/composio/composio_api.py +116 -136
- langflow/components/composio/dropbox_compnent.py +11 -0
- langflow/components/composio/github_composio.py +1 -639
- langflow/components/composio/gmail_composio.py +26 -394
- langflow/components/composio/googlecalendar_composio.py +2 -778
- langflow/components/composio/googlemeet_composio.py +11 -0
- langflow/components/composio/googletasks_composio.py +8 -0
- langflow/components/composio/linear_composio.py +11 -0
- langflow/components/composio/outlook_composio.py +1 -755
- langflow/components/composio/reddit_composio.py +11 -0
- langflow/components/composio/slack_composio.py +1 -576
- langflow/components/composio/slackbot_composio.py +11 -0
- langflow/components/composio/supabase_composio.py +11 -0
- langflow/components/composio/todoist_composio.py +11 -0
- langflow/components/composio/youtube_composio.py +11 -0
- langflow/components/data/kb_ingest.py +15 -16
- langflow/components/processing/save_file.py +31 -4
- langflow/custom/utils.py +30 -7
- langflow/frontend/assets/{SlackIcon-Bikuxo8x.js → SlackIcon-B260Qg_R.js} +1 -1
- langflow/frontend/assets/{Wikipedia-B6aCFf5-.js → Wikipedia-BB2mbgyd.js} +1 -1
- langflow/frontend/assets/{Wolfram-CekL_M-a.js → Wolfram-DytXC9hF.js} +1 -1
- langflow/frontend/assets/{index-D1RgjMON.js → index-3TJWUdmx.js} +1 -1
- langflow/frontend/assets/{index-B4xLpgbM.js → index-3qMh9x6K.js} +1 -1
- langflow/frontend/assets/{index-DEuXrfXH.js → index-3uOAA_XX.js} +1 -1
- langflow/frontend/assets/{index-DTJX3yQa.js → index-4eRtaV45.js} +1 -1
- langflow/frontend/assets/index-7xXgqu09.js +1 -0
- langflow/frontend/assets/{index-BRNhftot.js → index-AY5Dm2mG.js} +1 -1
- langflow/frontend/assets/{index-4Tl3Nxdo.js → index-AlJ7td-D.js} +1 -1
- langflow/frontend/assets/{index-D2nHdRne.js → index-B-c82Fnu.js} +1 -1
- langflow/frontend/assets/{index-C3RZz8WE.js → index-B2ggrBuR.js} +1 -1
- langflow/frontend/assets/{index-in188l0A.js → index-B536IPXH.js} +1 -1
- langflow/frontend/assets/{index-CP0tFKwN.js → index-B5ed-sAv.js} +1 -1
- langflow/frontend/assets/{index-CAzSTGAM.js → index-B8TlNgn-.js} +1 -1
- langflow/frontend/assets/{index-09CVJwsY.js → index-B8y58M9b.js} +1 -1
- langflow/frontend/assets/{index-B9uOBe6Y.js → index-B9Mo3ndZ.js} +1 -1
- langflow/frontend/assets/{index-DAJafn16.js → index-BCK-ZyIh.js} +1 -1
- langflow/frontend/assets/{index-Cy-ZEfWh.js → index-BEDxAk3N.js} +1 -1
- langflow/frontend/assets/{index-DbmqjLy6.js → index-BEKoRwsX.js} +1 -1
- langflow/frontend/assets/{index-BcqeL_f4.js → index-BIkqesA-.js} +1 -1
- langflow/frontend/assets/{index-7x3wNZ-4.js → index-BJrY2Fiu.js} +1 -1
- langflow/frontend/assets/{index-Iamzh9ZT.js → index-BKvKC-12.js} +1 -1
- langflow/frontend/assets/{index-COqjpsdy.js → index-BLROcaSz.js} +1 -1
- langflow/frontend/assets/{index-BRwkzs92.js → index-BNbWMmAV.js} +1 -1
- langflow/frontend/assets/{index-C_UkF-RJ.js → index-BOEf7-ty.js} +1 -1
- langflow/frontend/assets/index-BOYTBrh9.js +1 -0
- langflow/frontend/assets/{index-DDcpxWU4.js → index-BQB-iDYl.js} +1 -1
- langflow/frontend/assets/{index-Crq_yhkG.js → index-BRWNIt9F.js} +1 -1
- langflow/frontend/assets/{index-DmaQAn3K.js → index-BVHvIhT5.js} +1 -1
- langflow/frontend/assets/{index-Cs_jt3dj.js → index-BVtf6m9S.js} +1 -1
- langflow/frontend/assets/{index-T2jJOG85.js → index-BWq9GTzt.js} +1 -1
- langflow/frontend/assets/{index-Dz0r9Idb.js → index-BXMhmvTj.js} +1 -1
- langflow/frontend/assets/{index-eJwu5YEi.js → index-Ba3RTMXI.js} +1 -1
- langflow/frontend/assets/{index-xVx59Op-.js → index-Baka5dKE.js} +1 -1
- langflow/frontend/assets/{index-DnusMCK1.js → index-BbsND1Qg.js} +1 -1
- langflow/frontend/assets/index-BcgB3rXH.js +1 -0
- langflow/frontend/assets/{index-CmiRgF_-.js → index-BdIWbCEL.js} +1 -1
- langflow/frontend/assets/{index-BllNr21U.js → index-BdYgKk1d.js} +1 -1
- langflow/frontend/assets/{index-BIKbxmIh.js → index-BeNby7qF.js} +1 -1
- langflow/frontend/assets/{index-CUe1ivTn.js → index-BejHxU5W.js} +1 -1
- langflow/frontend/assets/{index-CVphnxXi.js → index-Bisa4IQF.js} +1 -1
- langflow/frontend/assets/{index-Cr2oy5K2.js → index-BjENqyKe.js} +1 -1
- langflow/frontend/assets/{index-CEn_71Wk.js → index-BlBl2tvQ.js} +1 -1
- langflow/frontend/assets/{index-DOb9c2bf.js → index-BnLT29qW.js} +1 -1
- langflow/frontend/assets/{index-BRizlHaN.js → index-BqUeOc7Y.js} +1 -1
- langflow/frontend/assets/{index-D7nFs6oq.js → index-BsBWP-Dh.js} +1 -1
- langflow/frontend/assets/{index-BlRTHXW5.js → index-BtJ2o21k.js} +1 -1
- langflow/frontend/assets/{index-AOX7bbjJ.js → index-BxWXWRmZ.js} +1 -1
- langflow/frontend/assets/{index-B20KmxhS.js → index-BxkZkBgQ.js} +1 -1
- langflow/frontend/assets/{index-DoFlaGDx.js → index-Bxml6wXu.js} +1 -1
- langflow/frontend/assets/{index-B9KRIJFi.js → index-ByFXr9Iq.js} +1 -1
- langflow/frontend/assets/{index-CY6LUi4V.js → index-C2Xd7UkR.js} +1 -1
- langflow/frontend/assets/index-C76aBV_h.js +1 -0
- langflow/frontend/assets/{index-9gkURvG2.js → index-C7V5U9yH.js} +1 -1
- langflow/frontend/assets/{index-BDmbsLY2.js → index-C7x9R_Yo.js} +1 -1
- langflow/frontend/assets/{index-DI0zAExi.js → index-C8KD3LPb.js} +1 -1
- langflow/frontend/assets/{index-DzDNhMMW.js → index-C9N80hP8.js} +1 -1
- langflow/frontend/assets/{index-6GWpsedd.js → index-CDFLVFB4.js} +1 -1
- langflow/frontend/assets/{index-pkOi9P45.js → index-CF4dtI6S.js} +1 -1
- langflow/frontend/assets/{index-CdwjD4IX.js → index-CG7cp0nD.js} +1 -1
- langflow/frontend/assets/{index-J0pvFqLk.js → index-CHFO5O4g.js} +1 -1
- langflow/frontend/assets/{index-5G402gB8.js → index-CJwYfDBz.js} +1 -1
- langflow/frontend/assets/{index-BzCjyHto.js → index-CMGZGIx_.js} +1 -1
- langflow/frontend/assets/{index-Bm7a2vMS.js → index-COL0eiWI.js} +1 -1
- langflow/frontend/assets/{index-JHCxbvlW.js → index-CWWo2zOA.js} +1 -1
- langflow/frontend/assets/{index-C7wDSVVH.js → index-C_1RBTul.js} +1 -1
- langflow/frontend/assets/{index-BIjUtp6d.js → index-Ccb5B8zG.js} +1 -1
- langflow/frontend/assets/{index-yIh6-LZT.js → index-Cd5zuUUK.js} +1 -1
- langflow/frontend/assets/{index-CPIdMJkX.js → index-CkQ-bJ4G.js} +1 -1
- langflow/frontend/assets/{index-TRyDa01A.js → index-CkSzjCqM.js} +1 -1
- langflow/frontend/assets/{index-CSRizl2S.js → index-CoUlHbtg.js} +1 -1
- langflow/frontend/assets/index-Cpgkb0Q3.js +1 -0
- langflow/frontend/assets/{index-Cp7Pmn03.js → index-CqDUqHfd.js} +1 -1
- langflow/frontend/assets/{index-CGVDXKtN.js → index-Ct9_T9ox.js} +1 -1
- langflow/frontend/assets/{index-BwlYjc56.js → index-CvQ0w8Pj.js} +1 -1
- langflow/frontend/assets/{index-DkJCCraf.js → index-CwIxqYlT.js} +1 -1
- langflow/frontend/assets/{index-Bgd7yLoW.js → index-Cx__T92e.js} +1 -1
- langflow/frontend/assets/{index-RveG4dl9.js → index-D-zkHcob.js} +1 -1
- langflow/frontend/assets/{index-DVV_etfW.js → index-D0HmkH0H.js} +1 -1
- langflow/frontend/assets/{index-CglSqvB5.js → index-D0s9f6Re.js} +1 -1
- langflow/frontend/assets/{index-J98sU-1p.js → index-D5PeCofu.js} +1 -1
- langflow/frontend/assets/{index-BJIsQS8D.js → index-D87Zw62M.js} +1 -1
- langflow/frontend/assets/{index-FYcoJPMP.js → index-D9eflZfP.js} +1 -1
- langflow/frontend/assets/{index-DJs6FoYC.js → index-DDNNv4C0.js} +1 -1
- langflow/frontend/assets/index-DHlEwAxb.js +1 -0
- langflow/frontend/assets/{index-DqDQk0Cu.js → index-DIqSyDVO.js} +1 -1
- langflow/frontend/assets/{index-DOI0ceS-.js → index-DK8vNpXK.js} +1 -1
- langflow/frontend/assets/{index-D29n5mus.js → index-DKEXZFUO.js} +1 -1
- langflow/frontend/assets/{index-dfaj9-hY.js → index-DPX6X_bw.js} +1 -1
- langflow/frontend/assets/{index-CgbINWS8.js → index-DS1EgA10.js} +1 -1
- langflow/frontend/assets/{index-C69gdJqw.js → index-DS9I4y48.js} +1 -1
- langflow/frontend/assets/{index-B2EmwqKj.js → index-DWkMJnbd.js} +1 -1
- langflow/frontend/assets/{index-CIYzjH2y.js → index-DWr_zPkx.js} +1 -1
- langflow/frontend/assets/{index-D-HTZ68O.js → index-DX7XsAcx.js} +1 -1
- langflow/frontend/assets/{index-Cq30cQcP.js → index-DZzbmg3J.js} +1 -1
- langflow/frontend/assets/{index-BZCt_UnJ.js → index-DasrI03Y.js} +1 -1
- langflow/frontend/assets/index-DdzVmJHE.js +1 -0
- langflow/frontend/assets/{index-DmvjdU1N.js → index-DhzEUXfr.js} +1 -1
- langflow/frontend/assets/{index-B_ytx_iA.js → index-DpJiH-Rk.js} +1 -1
- langflow/frontend/assets/{index-Cyk3aCmP.js → index-DpQKtcXu.js} +1 -1
- langflow/frontend/assets/{index-DrvRK4_i.js → index-Dpz3oBf5.js} +1 -1
- langflow/frontend/assets/{index-DF0oWRdd.js → index-DqSH4x-R.js} +1 -1
- langflow/frontend/assets/{index-DX_InNVT.js → index-DtJyCbzF.js} +1 -1
- langflow/frontend/assets/{index-B4AtFbkN.js → index-Du9aJK7m.js} +1 -1
- langflow/frontend/assets/{index-qXcoVIRo.js → index-DuAeoC-H.js} +1 -1
- langflow/frontend/assets/{index-D7Vx6mgS.js → index-DxIs8VSp.js} +1 -1
- langflow/frontend/assets/{index-U7J1YiWE.js → index-DyJDHm2D.js} +1 -1
- langflow/frontend/assets/{index-1MEYR1La.js → index-DzeIsaBm.js} +1 -1
- langflow/frontend/assets/{index-Cbwk3f-p.js → index-DztLFiip.js} +1 -1
- langflow/frontend/assets/{index-C_2G2ZqJ.js → index-GODbXlHC.js} +1 -1
- langflow/frontend/assets/{index-2vQdFIK_.js → index-G_U_kPAd.js} +1 -1
- langflow/frontend/assets/{index-DS4F_Phe.js → index-IFGgPiye.js} +1 -1
- langflow/frontend/assets/{index-5hW8VleF.js → index-LrMzDsq9.js} +1 -1
- langflow/frontend/assets/{index-L7FKc9QN.js → index-R7q8cAek.js} +1 -1
- langflow/frontend/assets/{index-BRE8A4Q_.js → index-Uq2ij_SS.js} +1 -1
- langflow/frontend/assets/{index-Bn4HAVDG.js → index-VHmUHUUU.js} +1 -1
- langflow/frontend/assets/{index-VO-pk-Hg.js → index-VZnN0P6C.js} +1 -1
- langflow/frontend/assets/{index-Dy7ehgeV.js → index-VcXZzovW.js} +1 -1
- langflow/frontend/assets/{index-DNS4La1f.js → index-Ym6gz0T6.js} +1 -1
- langflow/frontend/assets/{index-UI2ws3qp.js → index-ci4XHjbJ.js} +176 -176
- langflow/frontend/assets/{index-DlMAYATX.js → index-dkS0ek2S.js} +1 -1
- langflow/frontend/assets/{index-Dc0p1Oxl.js → index-hOkEW3JP.js} +1 -1
- langflow/frontend/assets/{index-KnS52ylc.js → index-js8ceOaP.js} +1 -1
- langflow/frontend/assets/{index-DtCsjX48.js → index-lKEJpUsF.js} +1 -1
- langflow/frontend/assets/{index-BO4fl1uU.js → index-mBjJYD9q.js} +1 -1
- langflow/frontend/assets/{index-C_K6Tof7.js → index-r1LZg-PY.js} +1 -1
- langflow/frontend/assets/index-rcdQpNcU.js +1 -0
- langflow/frontend/assets/{index-_3qag0I4.js → index-sS6XLk3j.js} +1 -1
- langflow/frontend/assets/{index-C6P0vvSP.js → index-tOy_uloT.js} +1 -1
- langflow/frontend/assets/lazyIconImports-Bh1TFfvH.js +2 -0
- langflow/frontend/assets/{use-post-add-user-Bt6vZvvT.js → use-post-add-user-HN0rRnhv.js} +1 -1
- langflow/frontend/index.html +1 -1
- langflow/initial_setup/starter_projects/Knowledge Ingestion.json +2 -2
- langflow/initial_setup/starter_projects/News Aggregator.json +19 -2
- langflow/initial_setup/starter_projects/Nvidia Remix.json +19 -2
- langflow/interface/initialize/loading.py +3 -1
- langflow/main.py +19 -2
- langflow/services/database/models/file/model.py +4 -2
- langflow/services/database/service.py +3 -1
- langflow/services/telemetry/schema.py +7 -0
- langflow/services/telemetry/service.py +25 -0
- langflow/services/tracing/service.py +14 -4
- {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/METADATA +1 -1
- {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/RECORD +170 -152
- langflow/frontend/assets/lazyIconImports-kvf_Kak2.js +0 -2
- {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/WHEEL +0 -0
- {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Modify uniqueness constraint on file names
|
|
2
|
+
|
|
3
|
+
Revision ID: 1cb603706752
|
|
4
|
+
Revises: 3162e83e485f
|
|
5
|
+
Create Date: 2025-07-24 07:02:14.896583
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from typing import Sequence, Union, Iterable, Optional, Set, Tuple
|
|
14
|
+
|
|
15
|
+
from alembic import op
|
|
16
|
+
import sqlalchemy as sa
|
|
17
|
+
from sqlalchemy import inspect
|
|
18
|
+
|
|
19
|
+
# revision identifiers, used by Alembic.
|
|
20
|
+
revision: str = "1cb603706752"
|
|
21
|
+
down_revision: Union[str, None] = "3162e83e485f"
|
|
22
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
23
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Behavior constants
|
|
28
|
+
DUPLICATE_SUFFIX_START = 2 # first suffix to use, e.g., "name_2.ext"
|
|
29
|
+
BATCH_SIZE = 1000 # Process duplicates in batches for large datasets
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_unique_constraints_by_columns(
|
|
33
|
+
inspector, table: str, expected_cols: Iterable[str]
|
|
34
|
+
) -> Optional[str]:
|
|
35
|
+
"""Return the name of a unique constraint that matches the exact set of expected columns."""
|
|
36
|
+
expected = set(expected_cols)
|
|
37
|
+
for c in inspector.get_unique_constraints(table):
|
|
38
|
+
cols = set(c.get("column_names") or [])
|
|
39
|
+
if cols == expected:
|
|
40
|
+
return c.get("name")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _split_base_ext(name: str) -> Tuple[str, str]:
|
|
45
|
+
"""Split a filename into (base, ext) where ext does not include the leading dot; ext may be ''."""
|
|
46
|
+
if "." in name:
|
|
47
|
+
base, ext = name.rsplit(".", 1)
|
|
48
|
+
return base, ext
|
|
49
|
+
return name, ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _escape_like(s: str) -> str:
|
|
53
|
+
# escape backslash first, then SQL LIKE wildcards
|
|
54
|
+
return s.replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _like_for_suffixes(base: str, ext: str) -> str:
|
|
58
|
+
eb = _escape_like(base)
|
|
59
|
+
if ext:
|
|
60
|
+
ex = ext.replace("%", r"\%").replace("_", r"\_")
|
|
61
|
+
return f"{eb}\\_%." + ex # literal underscore
|
|
62
|
+
else:
|
|
63
|
+
return f"{eb}\\_%"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _next_available_name(conn, user_id: str, base_name: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Compute the next available non-conflicting name for a given user.
|
|
69
|
+
Handles names with or without extensions and existing _N suffixes.
|
|
70
|
+
"""
|
|
71
|
+
base, ext = _split_base_ext(base_name)
|
|
72
|
+
|
|
73
|
+
# Load all sibling names once
|
|
74
|
+
rows = conn.execute(
|
|
75
|
+
sa.text("""
|
|
76
|
+
SELECT name
|
|
77
|
+
FROM file
|
|
78
|
+
WHERE user_id = :uid
|
|
79
|
+
AND (name = :base_name OR name LIKE :like ESCAPE '\\')
|
|
80
|
+
"""),
|
|
81
|
+
{"uid": user_id, "base_name": base_name, "like": _like_for_suffixes(base, ext)},
|
|
82
|
+
).scalars().all()
|
|
83
|
+
|
|
84
|
+
taken: Set[str] = set(rows)
|
|
85
|
+
|
|
86
|
+
# Pattern to detect base_N(.ext) and capture N
|
|
87
|
+
if ext:
|
|
88
|
+
rx = re.compile(rf"^{re.escape(base)}_(\d+)\.{re.escape(ext)}$")
|
|
89
|
+
else:
|
|
90
|
+
rx = re.compile(rf"^{re.escape(base)}_(\d+)$")
|
|
91
|
+
|
|
92
|
+
max_n = 1
|
|
93
|
+
for n in rows:
|
|
94
|
+
m = rx.match(n)
|
|
95
|
+
if m:
|
|
96
|
+
max_n = max(max_n, int(m.group(1)))
|
|
97
|
+
|
|
98
|
+
n = max(max_n + 1, DUPLICATE_SUFFIX_START)
|
|
99
|
+
while True:
|
|
100
|
+
candidate = f"{base}_{n}.{ext}" if ext else f"{base}_{n}"
|
|
101
|
+
if candidate not in taken:
|
|
102
|
+
return candidate
|
|
103
|
+
n += 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _handle_duplicates_before_upgrade(conn) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Ensure (user_id, name) is unique by renaming older duplicates before adding the composite unique constraint.
|
|
109
|
+
Keeps the most recently updated/created/id-highest record; renames the rest with _N suffix.
|
|
110
|
+
"""
|
|
111
|
+
logger.info("Scanning for duplicate file names per user...")
|
|
112
|
+
duplicates = conn.execute(
|
|
113
|
+
sa.text(
|
|
114
|
+
"""
|
|
115
|
+
SELECT user_id, name, COUNT(*) AS cnt
|
|
116
|
+
FROM file
|
|
117
|
+
GROUP BY user_id, name
|
|
118
|
+
HAVING COUNT(*) > 1
|
|
119
|
+
"""
|
|
120
|
+
)
|
|
121
|
+
).fetchall()
|
|
122
|
+
|
|
123
|
+
if not duplicates:
|
|
124
|
+
logger.info("No duplicates found.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
logger.info("Found %d duplicate sets. Resolving...", len(duplicates))
|
|
128
|
+
|
|
129
|
+
# Add progress indicator for large datasets
|
|
130
|
+
if len(duplicates) > 100:
|
|
131
|
+
logger.info("Large number of duplicates detected. This may take several minutes...")
|
|
132
|
+
|
|
133
|
+
# Wrap in a nested transaction so we fail cleanly on any error
|
|
134
|
+
with conn.begin_nested():
|
|
135
|
+
# Process duplicates in batches for better performance on large datasets
|
|
136
|
+
for batch_start in range(0, len(duplicates), BATCH_SIZE):
|
|
137
|
+
batch_end = min(batch_start + BATCH_SIZE, len(duplicates))
|
|
138
|
+
batch = duplicates[batch_start:batch_end]
|
|
139
|
+
|
|
140
|
+
if len(duplicates) > BATCH_SIZE:
|
|
141
|
+
logger.info("Processing batch %d-%d of %d duplicate sets...",
|
|
142
|
+
batch_start + 1, batch_end, len(duplicates))
|
|
143
|
+
|
|
144
|
+
for user_id, name, cnt in batch:
|
|
145
|
+
logger.debug("Resolving duplicates for user=%s, name=%r (count=%s)", user_id, name, cnt)
|
|
146
|
+
|
|
147
|
+
file_ids = conn.execute(
|
|
148
|
+
sa.text(
|
|
149
|
+
"""
|
|
150
|
+
SELECT id
|
|
151
|
+
FROM file
|
|
152
|
+
WHERE user_id = :uid AND name = :name
|
|
153
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
154
|
+
"""
|
|
155
|
+
),
|
|
156
|
+
{"uid": user_id, "name": name},
|
|
157
|
+
).scalars().all()
|
|
158
|
+
|
|
159
|
+
# Keep the first (most recent), rename the rest
|
|
160
|
+
for file_id in file_ids[1:]:
|
|
161
|
+
new_name = _next_available_name(conn, user_id, name)
|
|
162
|
+
conn.execute(
|
|
163
|
+
sa.text("UPDATE file SET name = :new_name WHERE id = :fid"),
|
|
164
|
+
{"new_name": new_name, "fid": file_id},
|
|
165
|
+
)
|
|
166
|
+
logger.debug("Renamed id=%s: %r -> %r", file_id, name, new_name)
|
|
167
|
+
|
|
168
|
+
# Progress update for large batches
|
|
169
|
+
if len(duplicates) > BATCH_SIZE and batch_end < len(duplicates):
|
|
170
|
+
logger.info("Completed %d of %d duplicate sets (%.1f%%)",
|
|
171
|
+
batch_end, len(duplicates), (batch_end / len(duplicates)) * 100)
|
|
172
|
+
|
|
173
|
+
logger.info("Duplicate resolution completed.")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def upgrade() -> None:
|
|
177
|
+
start_time = time.time()
|
|
178
|
+
logger.info("Starting upgrade: adding composite unique (name, user_id) on file")
|
|
179
|
+
|
|
180
|
+
conn = op.get_bind()
|
|
181
|
+
inspector = inspect(conn)
|
|
182
|
+
|
|
183
|
+
# 1) Resolve pre-existing duplicates so the new unique can be created
|
|
184
|
+
duplicate_start = time.time()
|
|
185
|
+
_handle_duplicates_before_upgrade(conn)
|
|
186
|
+
duplicate_duration = time.time() - duplicate_start
|
|
187
|
+
|
|
188
|
+
if duplicate_duration > 1.0: # Only log if it took more than 1 second
|
|
189
|
+
logger.info("Duplicate resolution completed in %.2f seconds", duplicate_duration)
|
|
190
|
+
|
|
191
|
+
# 2) Detect existing single-column unique on name (if any)
|
|
192
|
+
inspector = inspect(conn) # refresh inspector
|
|
193
|
+
single_name_uc = _get_unique_constraints_by_columns(inspector, "file", {"name"})
|
|
194
|
+
composite_uc = _get_unique_constraints_by_columns(inspector, "file", {"name", "user_id"})
|
|
195
|
+
|
|
196
|
+
# 3) Use a unified, reflection-based batch_alter_table for both Postgres and SQLite.
|
|
197
|
+
# recreate="always" ensures a safe table rebuild on SQLite and a standard alter on Postgres.
|
|
198
|
+
constraint_start = time.time()
|
|
199
|
+
with op.batch_alter_table("file", recreate="always") as batch_op:
|
|
200
|
+
# Drop old single-column unique if present
|
|
201
|
+
if single_name_uc:
|
|
202
|
+
logger.info("Dropping existing single-column unique: %s", single_name_uc)
|
|
203
|
+
batch_op.drop_constraint(single_name_uc, type_="unique")
|
|
204
|
+
|
|
205
|
+
# Create composite unique if not already present
|
|
206
|
+
if not composite_uc:
|
|
207
|
+
logger.info("Creating composite unique: file_name_user_id_key on (name, user_id)")
|
|
208
|
+
batch_op.create_unique_constraint("file_name_user_id_key", ["name", "user_id"])
|
|
209
|
+
else:
|
|
210
|
+
logger.info("Composite unique already present: %s", composite_uc)
|
|
211
|
+
|
|
212
|
+
constraint_duration = time.time() - constraint_start
|
|
213
|
+
if constraint_duration > 1.0: # Only log if it took more than 1 second
|
|
214
|
+
logger.info("Constraint operations completed in %.2f seconds", constraint_duration)
|
|
215
|
+
|
|
216
|
+
total_duration = time.time() - start_time
|
|
217
|
+
logger.info("Upgrade completed successfully in %.2f seconds", total_duration)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def downgrade() -> None:
|
|
221
|
+
start_time = time.time()
|
|
222
|
+
logger.info("Starting downgrade: reverting to single-column unique on (name)")
|
|
223
|
+
|
|
224
|
+
conn = op.get_bind()
|
|
225
|
+
inspector = inspect(conn)
|
|
226
|
+
|
|
227
|
+
# 1) Ensure no cross-user duplicates on name (since we'll enforce global uniqueness on name)
|
|
228
|
+
logger.info("Checking for cross-user duplicate names prior to downgrade...")
|
|
229
|
+
validation_start = time.time()
|
|
230
|
+
|
|
231
|
+
dup_names = conn.execute(
|
|
232
|
+
sa.text(
|
|
233
|
+
"""
|
|
234
|
+
SELECT name, COUNT(*) AS cnt
|
|
235
|
+
FROM file
|
|
236
|
+
GROUP BY name
|
|
237
|
+
HAVING COUNT(*) > 1
|
|
238
|
+
"""
|
|
239
|
+
)
|
|
240
|
+
).fetchall()
|
|
241
|
+
|
|
242
|
+
validation_duration = time.time() - validation_start
|
|
243
|
+
if validation_duration > 1.0: # Only log if it took more than 1 second
|
|
244
|
+
logger.info("Validation completed in %.2f seconds", validation_duration)
|
|
245
|
+
|
|
246
|
+
if dup_names:
|
|
247
|
+
examples = [row[0] for row in dup_names[:10]]
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
"Downgrade aborted: duplicate names exist across users. "
|
|
250
|
+
f"Examples: {examples}{'...' if len(dup_names) > 10 else ''}. "
|
|
251
|
+
"Rename conflicting files before downgrading."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# 2) Detect constraints
|
|
255
|
+
inspector = inspect(conn) # refresh
|
|
256
|
+
composite_uc = _get_unique_constraints_by_columns(inspector, "file", {"name", "user_id"})
|
|
257
|
+
single_name_uc = _get_unique_constraints_by_columns(inspector, "file", {"name"})
|
|
258
|
+
|
|
259
|
+
# 3) Perform alteration using batch with reflect to preserve other objects
|
|
260
|
+
constraint_start = time.time()
|
|
261
|
+
with op.batch_alter_table("file", recreate="always") as batch_op:
|
|
262
|
+
if composite_uc:
|
|
263
|
+
logger.info("Dropping composite unique: %s", composite_uc)
|
|
264
|
+
batch_op.drop_constraint(composite_uc, type_="unique")
|
|
265
|
+
else:
|
|
266
|
+
logger.info("No composite unique found to drop.")
|
|
267
|
+
|
|
268
|
+
if not single_name_uc:
|
|
269
|
+
logger.info("Creating single-column unique: file_name_key on (name)")
|
|
270
|
+
batch_op.create_unique_constraint("file_name_key", ["name"])
|
|
271
|
+
else:
|
|
272
|
+
logger.info("Single-column unique already present: %s", single_name_uc)
|
|
273
|
+
|
|
274
|
+
constraint_duration = time.time() - constraint_start
|
|
275
|
+
if constraint_duration > 1.0: # Only log if it took more than 1 second
|
|
276
|
+
logger.info("Constraint operations completed in %.2f seconds", constraint_duration)
|
|
277
|
+
|
|
278
|
+
total_duration = time.time() - start_time
|
|
279
|
+
logger.info("Downgrade completed successfully in %.2f seconds", total_duration)
|
langflow/api/v1/endpoints.py
CHANGED
|
@@ -724,7 +724,7 @@ async def custom_component_update(
|
|
|
724
724
|
field_value=code_request.field_value,
|
|
725
725
|
field_name=code_request.field,
|
|
726
726
|
)
|
|
727
|
-
if "code" not in updated_build_config:
|
|
727
|
+
if "code" not in updated_build_config or not updated_build_config.get("code", {}).get("value"):
|
|
728
728
|
updated_build_config = add_code_field_to_build_config(updated_build_config, code_request.code)
|
|
729
729
|
component_node["template"] = updated_build_config
|
|
730
730
|
|