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.
Files changed (171) hide show
  1. langflow/alembic/versions/1cb603706752_modify_uniqueness_constraint_on_file_.py +279 -0
  2. langflow/api/v1/endpoints.py +1 -1
  3. langflow/base/composio/composio_base.py +1092 -126
  4. langflow/components/agents/mcp_component.py +21 -4
  5. langflow/components/composio/__init__.py +24 -0
  6. langflow/components/composio/composio_api.py +116 -136
  7. langflow/components/composio/dropbox_compnent.py +11 -0
  8. langflow/components/composio/github_composio.py +1 -639
  9. langflow/components/composio/gmail_composio.py +26 -394
  10. langflow/components/composio/googlecalendar_composio.py +2 -778
  11. langflow/components/composio/googlemeet_composio.py +11 -0
  12. langflow/components/composio/googletasks_composio.py +8 -0
  13. langflow/components/composio/linear_composio.py +11 -0
  14. langflow/components/composio/outlook_composio.py +1 -755
  15. langflow/components/composio/reddit_composio.py +11 -0
  16. langflow/components/composio/slack_composio.py +1 -576
  17. langflow/components/composio/slackbot_composio.py +11 -0
  18. langflow/components/composio/supabase_composio.py +11 -0
  19. langflow/components/composio/todoist_composio.py +11 -0
  20. langflow/components/composio/youtube_composio.py +11 -0
  21. langflow/components/data/kb_ingest.py +15 -16
  22. langflow/components/processing/save_file.py +31 -4
  23. langflow/custom/utils.py +30 -7
  24. langflow/frontend/assets/{SlackIcon-Bikuxo8x.js → SlackIcon-B260Qg_R.js} +1 -1
  25. langflow/frontend/assets/{Wikipedia-B6aCFf5-.js → Wikipedia-BB2mbgyd.js} +1 -1
  26. langflow/frontend/assets/{Wolfram-CekL_M-a.js → Wolfram-DytXC9hF.js} +1 -1
  27. langflow/frontend/assets/{index-D1RgjMON.js → index-3TJWUdmx.js} +1 -1
  28. langflow/frontend/assets/{index-B4xLpgbM.js → index-3qMh9x6K.js} +1 -1
  29. langflow/frontend/assets/{index-DEuXrfXH.js → index-3uOAA_XX.js} +1 -1
  30. langflow/frontend/assets/{index-DTJX3yQa.js → index-4eRtaV45.js} +1 -1
  31. langflow/frontend/assets/index-7xXgqu09.js +1 -0
  32. langflow/frontend/assets/{index-BRNhftot.js → index-AY5Dm2mG.js} +1 -1
  33. langflow/frontend/assets/{index-4Tl3Nxdo.js → index-AlJ7td-D.js} +1 -1
  34. langflow/frontend/assets/{index-D2nHdRne.js → index-B-c82Fnu.js} +1 -1
  35. langflow/frontend/assets/{index-C3RZz8WE.js → index-B2ggrBuR.js} +1 -1
  36. langflow/frontend/assets/{index-in188l0A.js → index-B536IPXH.js} +1 -1
  37. langflow/frontend/assets/{index-CP0tFKwN.js → index-B5ed-sAv.js} +1 -1
  38. langflow/frontend/assets/{index-CAzSTGAM.js → index-B8TlNgn-.js} +1 -1
  39. langflow/frontend/assets/{index-09CVJwsY.js → index-B8y58M9b.js} +1 -1
  40. langflow/frontend/assets/{index-B9uOBe6Y.js → index-B9Mo3ndZ.js} +1 -1
  41. langflow/frontend/assets/{index-DAJafn16.js → index-BCK-ZyIh.js} +1 -1
  42. langflow/frontend/assets/{index-Cy-ZEfWh.js → index-BEDxAk3N.js} +1 -1
  43. langflow/frontend/assets/{index-DbmqjLy6.js → index-BEKoRwsX.js} +1 -1
  44. langflow/frontend/assets/{index-BcqeL_f4.js → index-BIkqesA-.js} +1 -1
  45. langflow/frontend/assets/{index-7x3wNZ-4.js → index-BJrY2Fiu.js} +1 -1
  46. langflow/frontend/assets/{index-Iamzh9ZT.js → index-BKvKC-12.js} +1 -1
  47. langflow/frontend/assets/{index-COqjpsdy.js → index-BLROcaSz.js} +1 -1
  48. langflow/frontend/assets/{index-BRwkzs92.js → index-BNbWMmAV.js} +1 -1
  49. langflow/frontend/assets/{index-C_UkF-RJ.js → index-BOEf7-ty.js} +1 -1
  50. langflow/frontend/assets/index-BOYTBrh9.js +1 -0
  51. langflow/frontend/assets/{index-DDcpxWU4.js → index-BQB-iDYl.js} +1 -1
  52. langflow/frontend/assets/{index-Crq_yhkG.js → index-BRWNIt9F.js} +1 -1
  53. langflow/frontend/assets/{index-DmaQAn3K.js → index-BVHvIhT5.js} +1 -1
  54. langflow/frontend/assets/{index-Cs_jt3dj.js → index-BVtf6m9S.js} +1 -1
  55. langflow/frontend/assets/{index-T2jJOG85.js → index-BWq9GTzt.js} +1 -1
  56. langflow/frontend/assets/{index-Dz0r9Idb.js → index-BXMhmvTj.js} +1 -1
  57. langflow/frontend/assets/{index-eJwu5YEi.js → index-Ba3RTMXI.js} +1 -1
  58. langflow/frontend/assets/{index-xVx59Op-.js → index-Baka5dKE.js} +1 -1
  59. langflow/frontend/assets/{index-DnusMCK1.js → index-BbsND1Qg.js} +1 -1
  60. langflow/frontend/assets/index-BcgB3rXH.js +1 -0
  61. langflow/frontend/assets/{index-CmiRgF_-.js → index-BdIWbCEL.js} +1 -1
  62. langflow/frontend/assets/{index-BllNr21U.js → index-BdYgKk1d.js} +1 -1
  63. langflow/frontend/assets/{index-BIKbxmIh.js → index-BeNby7qF.js} +1 -1
  64. langflow/frontend/assets/{index-CUe1ivTn.js → index-BejHxU5W.js} +1 -1
  65. langflow/frontend/assets/{index-CVphnxXi.js → index-Bisa4IQF.js} +1 -1
  66. langflow/frontend/assets/{index-Cr2oy5K2.js → index-BjENqyKe.js} +1 -1
  67. langflow/frontend/assets/{index-CEn_71Wk.js → index-BlBl2tvQ.js} +1 -1
  68. langflow/frontend/assets/{index-DOb9c2bf.js → index-BnLT29qW.js} +1 -1
  69. langflow/frontend/assets/{index-BRizlHaN.js → index-BqUeOc7Y.js} +1 -1
  70. langflow/frontend/assets/{index-D7nFs6oq.js → index-BsBWP-Dh.js} +1 -1
  71. langflow/frontend/assets/{index-BlRTHXW5.js → index-BtJ2o21k.js} +1 -1
  72. langflow/frontend/assets/{index-AOX7bbjJ.js → index-BxWXWRmZ.js} +1 -1
  73. langflow/frontend/assets/{index-B20KmxhS.js → index-BxkZkBgQ.js} +1 -1
  74. langflow/frontend/assets/{index-DoFlaGDx.js → index-Bxml6wXu.js} +1 -1
  75. langflow/frontend/assets/{index-B9KRIJFi.js → index-ByFXr9Iq.js} +1 -1
  76. langflow/frontend/assets/{index-CY6LUi4V.js → index-C2Xd7UkR.js} +1 -1
  77. langflow/frontend/assets/index-C76aBV_h.js +1 -0
  78. langflow/frontend/assets/{index-9gkURvG2.js → index-C7V5U9yH.js} +1 -1
  79. langflow/frontend/assets/{index-BDmbsLY2.js → index-C7x9R_Yo.js} +1 -1
  80. langflow/frontend/assets/{index-DI0zAExi.js → index-C8KD3LPb.js} +1 -1
  81. langflow/frontend/assets/{index-DzDNhMMW.js → index-C9N80hP8.js} +1 -1
  82. langflow/frontend/assets/{index-6GWpsedd.js → index-CDFLVFB4.js} +1 -1
  83. langflow/frontend/assets/{index-pkOi9P45.js → index-CF4dtI6S.js} +1 -1
  84. langflow/frontend/assets/{index-CdwjD4IX.js → index-CG7cp0nD.js} +1 -1
  85. langflow/frontend/assets/{index-J0pvFqLk.js → index-CHFO5O4g.js} +1 -1
  86. langflow/frontend/assets/{index-5G402gB8.js → index-CJwYfDBz.js} +1 -1
  87. langflow/frontend/assets/{index-BzCjyHto.js → index-CMGZGIx_.js} +1 -1
  88. langflow/frontend/assets/{index-Bm7a2vMS.js → index-COL0eiWI.js} +1 -1
  89. langflow/frontend/assets/{index-JHCxbvlW.js → index-CWWo2zOA.js} +1 -1
  90. langflow/frontend/assets/{index-C7wDSVVH.js → index-C_1RBTul.js} +1 -1
  91. langflow/frontend/assets/{index-BIjUtp6d.js → index-Ccb5B8zG.js} +1 -1
  92. langflow/frontend/assets/{index-yIh6-LZT.js → index-Cd5zuUUK.js} +1 -1
  93. langflow/frontend/assets/{index-CPIdMJkX.js → index-CkQ-bJ4G.js} +1 -1
  94. langflow/frontend/assets/{index-TRyDa01A.js → index-CkSzjCqM.js} +1 -1
  95. langflow/frontend/assets/{index-CSRizl2S.js → index-CoUlHbtg.js} +1 -1
  96. langflow/frontend/assets/index-Cpgkb0Q3.js +1 -0
  97. langflow/frontend/assets/{index-Cp7Pmn03.js → index-CqDUqHfd.js} +1 -1
  98. langflow/frontend/assets/{index-CGVDXKtN.js → index-Ct9_T9ox.js} +1 -1
  99. langflow/frontend/assets/{index-BwlYjc56.js → index-CvQ0w8Pj.js} +1 -1
  100. langflow/frontend/assets/{index-DkJCCraf.js → index-CwIxqYlT.js} +1 -1
  101. langflow/frontend/assets/{index-Bgd7yLoW.js → index-Cx__T92e.js} +1 -1
  102. langflow/frontend/assets/{index-RveG4dl9.js → index-D-zkHcob.js} +1 -1
  103. langflow/frontend/assets/{index-DVV_etfW.js → index-D0HmkH0H.js} +1 -1
  104. langflow/frontend/assets/{index-CglSqvB5.js → index-D0s9f6Re.js} +1 -1
  105. langflow/frontend/assets/{index-J98sU-1p.js → index-D5PeCofu.js} +1 -1
  106. langflow/frontend/assets/{index-BJIsQS8D.js → index-D87Zw62M.js} +1 -1
  107. langflow/frontend/assets/{index-FYcoJPMP.js → index-D9eflZfP.js} +1 -1
  108. langflow/frontend/assets/{index-DJs6FoYC.js → index-DDNNv4C0.js} +1 -1
  109. langflow/frontend/assets/index-DHlEwAxb.js +1 -0
  110. langflow/frontend/assets/{index-DqDQk0Cu.js → index-DIqSyDVO.js} +1 -1
  111. langflow/frontend/assets/{index-DOI0ceS-.js → index-DK8vNpXK.js} +1 -1
  112. langflow/frontend/assets/{index-D29n5mus.js → index-DKEXZFUO.js} +1 -1
  113. langflow/frontend/assets/{index-dfaj9-hY.js → index-DPX6X_bw.js} +1 -1
  114. langflow/frontend/assets/{index-CgbINWS8.js → index-DS1EgA10.js} +1 -1
  115. langflow/frontend/assets/{index-C69gdJqw.js → index-DS9I4y48.js} +1 -1
  116. langflow/frontend/assets/{index-B2EmwqKj.js → index-DWkMJnbd.js} +1 -1
  117. langflow/frontend/assets/{index-CIYzjH2y.js → index-DWr_zPkx.js} +1 -1
  118. langflow/frontend/assets/{index-D-HTZ68O.js → index-DX7XsAcx.js} +1 -1
  119. langflow/frontend/assets/{index-Cq30cQcP.js → index-DZzbmg3J.js} +1 -1
  120. langflow/frontend/assets/{index-BZCt_UnJ.js → index-DasrI03Y.js} +1 -1
  121. langflow/frontend/assets/index-DdzVmJHE.js +1 -0
  122. langflow/frontend/assets/{index-DmvjdU1N.js → index-DhzEUXfr.js} +1 -1
  123. langflow/frontend/assets/{index-B_ytx_iA.js → index-DpJiH-Rk.js} +1 -1
  124. langflow/frontend/assets/{index-Cyk3aCmP.js → index-DpQKtcXu.js} +1 -1
  125. langflow/frontend/assets/{index-DrvRK4_i.js → index-Dpz3oBf5.js} +1 -1
  126. langflow/frontend/assets/{index-DF0oWRdd.js → index-DqSH4x-R.js} +1 -1
  127. langflow/frontend/assets/{index-DX_InNVT.js → index-DtJyCbzF.js} +1 -1
  128. langflow/frontend/assets/{index-B4AtFbkN.js → index-Du9aJK7m.js} +1 -1
  129. langflow/frontend/assets/{index-qXcoVIRo.js → index-DuAeoC-H.js} +1 -1
  130. langflow/frontend/assets/{index-D7Vx6mgS.js → index-DxIs8VSp.js} +1 -1
  131. langflow/frontend/assets/{index-U7J1YiWE.js → index-DyJDHm2D.js} +1 -1
  132. langflow/frontend/assets/{index-1MEYR1La.js → index-DzeIsaBm.js} +1 -1
  133. langflow/frontend/assets/{index-Cbwk3f-p.js → index-DztLFiip.js} +1 -1
  134. langflow/frontend/assets/{index-C_2G2ZqJ.js → index-GODbXlHC.js} +1 -1
  135. langflow/frontend/assets/{index-2vQdFIK_.js → index-G_U_kPAd.js} +1 -1
  136. langflow/frontend/assets/{index-DS4F_Phe.js → index-IFGgPiye.js} +1 -1
  137. langflow/frontend/assets/{index-5hW8VleF.js → index-LrMzDsq9.js} +1 -1
  138. langflow/frontend/assets/{index-L7FKc9QN.js → index-R7q8cAek.js} +1 -1
  139. langflow/frontend/assets/{index-BRE8A4Q_.js → index-Uq2ij_SS.js} +1 -1
  140. langflow/frontend/assets/{index-Bn4HAVDG.js → index-VHmUHUUU.js} +1 -1
  141. langflow/frontend/assets/{index-VO-pk-Hg.js → index-VZnN0P6C.js} +1 -1
  142. langflow/frontend/assets/{index-Dy7ehgeV.js → index-VcXZzovW.js} +1 -1
  143. langflow/frontend/assets/{index-DNS4La1f.js → index-Ym6gz0T6.js} +1 -1
  144. langflow/frontend/assets/{index-UI2ws3qp.js → index-ci4XHjbJ.js} +176 -176
  145. langflow/frontend/assets/{index-DlMAYATX.js → index-dkS0ek2S.js} +1 -1
  146. langflow/frontend/assets/{index-Dc0p1Oxl.js → index-hOkEW3JP.js} +1 -1
  147. langflow/frontend/assets/{index-KnS52ylc.js → index-js8ceOaP.js} +1 -1
  148. langflow/frontend/assets/{index-DtCsjX48.js → index-lKEJpUsF.js} +1 -1
  149. langflow/frontend/assets/{index-BO4fl1uU.js → index-mBjJYD9q.js} +1 -1
  150. langflow/frontend/assets/{index-C_K6Tof7.js → index-r1LZg-PY.js} +1 -1
  151. langflow/frontend/assets/index-rcdQpNcU.js +1 -0
  152. langflow/frontend/assets/{index-_3qag0I4.js → index-sS6XLk3j.js} +1 -1
  153. langflow/frontend/assets/{index-C6P0vvSP.js → index-tOy_uloT.js} +1 -1
  154. langflow/frontend/assets/lazyIconImports-Bh1TFfvH.js +2 -0
  155. langflow/frontend/assets/{use-post-add-user-Bt6vZvvT.js → use-post-add-user-HN0rRnhv.js} +1 -1
  156. langflow/frontend/index.html +1 -1
  157. langflow/initial_setup/starter_projects/Knowledge Ingestion.json +2 -2
  158. langflow/initial_setup/starter_projects/News Aggregator.json +19 -2
  159. langflow/initial_setup/starter_projects/Nvidia Remix.json +19 -2
  160. langflow/interface/initialize/loading.py +3 -1
  161. langflow/main.py +19 -2
  162. langflow/services/database/models/file/model.py +4 -2
  163. langflow/services/database/service.py +3 -1
  164. langflow/services/telemetry/schema.py +7 -0
  165. langflow/services/telemetry/service.py +25 -0
  166. langflow/services/tracing/service.py +14 -4
  167. {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/METADATA +1 -1
  168. {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/RECORD +170 -152
  169. langflow/frontend/assets/lazyIconImports-kvf_Kak2.js +0 -2
  170. {langflow_base_nightly-0.5.0.dev33.dist-info → langflow_base_nightly-0.5.0.dev35.dist-info}/WHEEL +0 -0
  171. {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)
@@ -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