Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit 97977ad

Browse files
committed
feat(sdf-server, dal): Exec variant def creates new variant, copies funcs
Instead of deleting the old schema variant and creating a new one, then attempting to migrate components, we just create a new schema variant on exec_variant_def and mark it as the default variant. Existing components are unchanged. Component upgrades are coming next. This is controlled by the "multiVariantEditing" flag, which is not yet in posthog (but can be easily turned on in the feature flags store).
1 parent 78d2e7a commit 97977ad

File tree

17 files changed

+506
-184
lines changed

17 files changed

+506
-184
lines changed

app/web/src/components/AssetDetailsPanel.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ import { FuncVariant } from "@/api/sdf/dal/func";
182182
import { useAssetStore } from "@/store/asset.store";
183183
import { FuncId } from "@/store/func/funcs.store";
184184
import { ComponentType } from "@/components/ModelingDiagram/diagram_types";
185+
import { useFeatureFlagsStore } from "@/store/feature_flags.store";
185186
import ColorPicker from "./ColorPicker.vue";
186187
import AssetFuncAttachModal from "./AssetFuncAttachModal.vue";
187188
@@ -196,6 +197,8 @@ const loadAssetReqStatus = assetStore.getRequestStatus(
196197
);
197198
const executeAssetModalRef = ref();
198199
200+
const featureFlagStore = useFeatureFlagsStore();
201+
199202
const openAttachModal = (warning: {
200203
variant?: FuncVariant;
201204
funcId?: FuncId;
@@ -244,10 +247,16 @@ const updateAsset = async () => {
244247
}
245248
};
246249
247-
const disabled = computed(() => !!(editingAsset.value?.hasComponents ?? false));
250+
const disabled = computed(() => {
251+
if (featureFlagStore.MULTI_VARIANT_EDITING) {
252+
return false;
253+
}
254+
255+
return !!(editingAsset.value?.hasComponents ?? false);
256+
});
248257
249258
const disabledWarning = computed(() => {
250-
if (editingAsset.value?.hasComponents) {
259+
if (disabled.value) {
251260
return `This asset cannot be edited because it is in use by components.`;
252261
}
253262

app/web/src/components/AssetEditor.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,26 @@ import {
5252
} from "@si/vue-lib/design-system";
5353
import { useAssetStore, assetDisplayName } from "@/store/asset.store";
5454
import SiChip from "@/components/SiChip.vue";
55+
import { useFeatureFlagsStore } from "@/store/feature_flags.store";
5556
import CodeEditor from "./CodeEditor.vue";
5657
import NodeSkeleton from "./NodeSkeleton.vue";
5758
5859
const props = defineProps<{
5960
assetId?: string;
6061
}>();
6162
63+
const featureFlagStore = useFeatureFlagsStore();
6264
const assetStore = useAssetStore();
6365
const selectedAsset = computed(() =>
6466
props.assetId ? assetStore.assetsById[props.assetId] : undefined,
6567
);
6668
67-
const isReadOnly = computed(() => !!selectedAsset.value?.hasComponents);
69+
const isReadOnly = computed(() => {
70+
if (featureFlagStore.MULTI_VARIANT_EDITING) {
71+
return false;
72+
}
73+
return !!selectedAsset.value?.hasComponents;
74+
});
6875
6976
const editingAsset = ref<string>(selectedAsset.value?.code ?? "");
7077

app/web/src/store/asset.store.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,15 @@ export type Asset = VariantDef;
9696
export type AssetListEntry = ListedVariantDef;
9797
export type AssetSaveRequest = Visibility & {
9898
overrideBuiltinSchemaFeatureFlag: boolean;
99+
multiVariantEditingFlag: boolean;
99100
} & Omit<Asset, "createdAt" | "updatedAt" | "variantExists" | "hasComponents">;
100101
export type AssetCreateRequest = Omit<
101102
AssetSaveRequest,
102-
"id" | "definition" | "variantExists" | "overrideBuiltinSchemaFeatureFlag"
103+
| "id"
104+
| "definition"
105+
| "variantExists"
106+
| "overrideBuiltinSchemaFeatureFlag"
107+
| "multiVariantEditingFlag"
103108
>;
104109
export type AssetCloneRequest = Visibility & { id: AssetId };
105110

@@ -350,6 +355,7 @@ export const useAssetStore = () => {
350355
params: {
351356
overrideBuiltinSchemaFeatureFlag:
352357
featureFlagsStore.OVERRIDE_SCHEMA,
358+
multiVariantEditingFlag: featureFlagsStore.MULTI_VARIANT_EDITING,
353359
...visibility,
354360
..._.omit(asset, [
355361
"schemaVariantId",
@@ -414,6 +420,7 @@ export const useAssetStore = () => {
414420
params: {
415421
overrideBuiltinSchemaFeatureFlag:
416422
featureFlagsStore.OVERRIDE_SCHEMA,
423+
multiVariantEditingFlag: featureFlagsStore.MULTI_VARIANT_EDITING,
417424
...visibility,
418425
..._.omit(asset, [
419426
"schemaVariantId",

app/web/src/store/feature_flags.store.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@ import { posthog } from "@/utils/posthog";
66
// translation from store key to posthog feature flag name
77
const FLAG_MAPPING = {
88
// STORE_FLAG_NAME: "posthogFlagName",
9-
MODULES_TAB: "modules_tab",
10-
SECRETS: "secrets",
11-
INVITE_USER: "invite_user",
12-
WORKSPACE_BACKUPS: "workspaceBackups",
9+
// STORE_FLAG_NAME: "posthogFlagName",
1310
COPY_PASTE: "copy_paste",
1411
DONT_BLOCK_ON_ACTIONS: "dont_block_on_actions",
15-
OVERRIDE_SCHEMA: "override_schema",
12+
INVITE_USER: "invite_user",
1613
JOI_VALIDATIONS: "joi_validations",
17-
SHOW_EDGES_ON_SELECT: "show_edges_on_select",
14+
MODULES_TAB: "modules_tab",
15+
MULTI_VARIANT_EDITING: "multiVariantEditing",
16+
OVERRIDE_SCHEMA: "override_schema",
1817
REMOVE_COMPONENT_ICONS: "remove_component_icons",
1918
RESIZABLE_PANEL_UPGRADE: "resizable-panel-upgrade",
19+
SECRETS: "secrets",
20+
SHOW_EDGES_ON_SELECT: "show_edges_on_select",
21+
WORKSPACE_BACKUPS: "workspaceBackups",
2022
};
2123

2224
type FeatureFlags = keyof typeof FLAG_MAPPING;
@@ -42,6 +44,8 @@ export function useFeatureFlagsStore() {
4244
});
4345
// You can override feature flags while working on a feature by setting them to true here
4446

47+
// this.MULTI_VARIANT_EDITING = true;
48+
4549
// Make sure to remove the override before committing your code!
4650
},
4751
}),

lib/dal/src/func.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,20 @@ impl Func {
165165

166166
/// Creates a new [`Func`] from [`self`](Func). All relevant fields are duplicated, but rows
167167
/// existing on relationship tables (e.g. "belongs_to" or "many_to_many") are not.
168-
pub async fn duplicate(&self, ctx: &DalContext) -> FuncResult<Self> {
169-
// Generate a unique name and make sure it's not in use
168+
pub async fn duplicate(&self, ctx: &DalContext, new_name: Option<String>) -> FuncResult<Self> {
169+
// Generate a unique name and make sure it's not in use, unless we were
170+
// passed in a name, in which case just use that
170171
let mut new_unique_name;
171-
loop {
172-
new_unique_name = format!("{}{}", self.name(), generate_unique_id(4));
173-
if Self::find_by_name(ctx, &new_unique_name).await?.is_none() {
174-
break;
175-
};
172+
match new_name {
173+
Some(new_name) => {
174+
new_unique_name = new_name;
175+
}
176+
None => loop {
177+
new_unique_name = format!("{}{}", self.name(), generate_unique_id(4));
178+
if Self::find_by_name(ctx, &new_unique_name).await?.is_none() {
179+
break;
180+
};
181+
},
176182
}
177183

178184
let mut new_func = Self::new(

lib/dal/src/pkg.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ use crate::{
3333
WsEvent, WsEventResult, WsPayload,
3434
};
3535

36-
mod export;
37-
mod import;
36+
pub mod export;
37+
pub mod import;
3838

3939
#[remain::sorted]
4040
#[derive(Debug, Error)]

lib/dal/src/pkg/export.rs

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ impl PkgExporter {
115115
}
116116
}
117117

118+
fn new_standalone_variant_exporter() -> Self {
119+
Self::new_module_exporter("", "", None::<String>, "", vec![])
120+
}
121+
118122
pub async fn export_as_bytes(&mut self, ctx: &DalContext) -> PkgResult<Vec<u8>> {
119123
match self.kind {
120124
SiPkgKind::Module => info!("Building module package"),
@@ -145,40 +149,19 @@ impl PkgExporter {
145149
}
146150

147151
let in_change_set = std_model_change_set_matches(change_set_pk, schema);
148-
let is_deleted = schema.visibility().is_deleted();
152+
let schema_is_deleted = schema.visibility().is_deleted();
149153

150154
let default_variant_id = schema.default_schema_variant_id().copied();
151155
let mut default_variant_unique_id = None;
152156

153157
for variant in &variants {
154-
let related_funcs = SchemaVariant::all_funcs(ctx, *variant.id()).await?;
155-
156-
for func in &related_funcs {
157-
if change_set_pk.is_some()
158-
&& change_set_pk.as_ref().expect("some is ensured") != &ChangeSetPk::NONE
159-
&& self.func_map.get(ChangeSetPk::NONE, func.id()).is_none()
160-
&& func.visibility().change_set_pk == ChangeSetPk::NONE
161-
{
162-
let (func_spec, _) =
163-
self.export_func(ctx, Some(ChangeSetPk::NONE), func).await?;
164-
self.func_map
165-
.insert(ChangeSetPk::NONE, *func.id(), func_spec.to_owned());
166-
head_funcs.push(func_spec);
167-
} else {
168-
let (func_spec, include) = self.export_func(ctx, change_set_pk, func).await?;
169-
self.func_map.insert(
170-
change_set_pk.unwrap_or(ChangeSetPk::NONE),
171-
*func.id(),
172-
func_spec.to_owned(),
173-
);
174-
175-
if include {
176-
funcs.push(func_spec);
177-
}
178-
}
179-
}
158+
let (variant_funcs, variant_head_funcs) = self
159+
.export_funcs_for_variant(ctx, change_set_pk, *variant.id())
160+
.await?;
161+
funcs.extend(variant_funcs);
162+
head_funcs.extend(variant_head_funcs);
180163

181-
if !is_deleted {
164+
if !schema_is_deleted {
182165
let variant_spec = self.export_variant(ctx, change_set_pk, variant).await?;
183166
self.variant_map.insert(
184167
change_set_pk.unwrap_or(ChangeSetPk::NONE),
@@ -196,7 +179,7 @@ impl PkgExporter {
196179
}
197180
}
198181

199-
if in_change_set && is_deleted {
182+
if in_change_set && schema_is_deleted {
200183
schema_spec_builder.deleted(true);
201184
} else if in_change_set {
202185
let mut data_builder = SchemaSpecData::builder();
@@ -222,6 +205,23 @@ impl PkgExporter {
222205
Ok((schema_spec, funcs, head_funcs))
223206
}
224207

208+
/// Exports just a single schema variant and the functions connected to it.
209+
/// Visiblity is taken from the context, so this will export the schema
210+
/// variant according to the normal rules of visibility.
211+
pub async fn export_variant_standalone(
212+
ctx: &DalContext,
213+
variant: &SchemaVariant,
214+
) -> PkgResult<(SchemaVariantSpec, Vec<FuncSpec>)> {
215+
let mut exporter = Self::new_standalone_variant_exporter();
216+
let (funcs, _) = exporter
217+
.export_funcs_for_variant(ctx, None, *variant.id())
218+
.await?;
219+
220+
let variant_spec = exporter.export_variant(ctx, None, variant).await?;
221+
222+
Ok((variant_spec, funcs))
223+
}
224+
225225
async fn export_variant(
226226
&self,
227227
ctx: &DalContext,
@@ -1672,6 +1672,43 @@ impl PkgExporter {
16721672

16731673
Ok(pkg)
16741674
}
1675+
1676+
async fn export_funcs_for_variant(
1677+
&mut self,
1678+
ctx: &DalContext,
1679+
change_set_pk: Option<ChangeSetPk>,
1680+
schema_variant_id: SchemaVariantId,
1681+
) -> PkgResult<(Vec<FuncSpec>, Vec<FuncSpec>)> {
1682+
let related_funcs = SchemaVariant::all_funcs(ctx, schema_variant_id).await?;
1683+
let mut head_funcs = vec![];
1684+
let mut funcs = vec![];
1685+
1686+
for func in &related_funcs {
1687+
if change_set_pk.is_some()
1688+
&& change_set_pk.as_ref().expect("some is ensured") != &ChangeSetPk::NONE
1689+
&& self.func_map.get(ChangeSetPk::NONE, func.id()).is_none()
1690+
&& func.visibility().change_set_pk == ChangeSetPk::NONE
1691+
{
1692+
let (func_spec, _) = self.export_func(ctx, Some(ChangeSetPk::NONE), func).await?;
1693+
self.func_map
1694+
.insert(ChangeSetPk::NONE, *func.id(), func_spec.to_owned());
1695+
head_funcs.push(func_spec);
1696+
} else {
1697+
let (func_spec, include) = self.export_func(ctx, change_set_pk, func).await?;
1698+
self.func_map.insert(
1699+
change_set_pk.unwrap_or(ChangeSetPk::NONE),
1700+
*func.id(),
1701+
func_spec.to_owned(),
1702+
);
1703+
1704+
if include {
1705+
funcs.push(func_spec);
1706+
}
1707+
}
1708+
}
1709+
1710+
Ok((funcs, head_funcs))
1711+
}
16751712
}
16761713

16771714
fn remove_duplicate_func_specs(func_specs: &[FuncSpec]) -> Vec<FuncSpec> {

lib/dal/src/pkg/import.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use crate::{
5555
use super::{PkgError, PkgResult};
5656

5757
#[derive(Clone, Debug)]
58-
enum Thing {
58+
pub enum Thing {
5959
ActionPrototype(ActionPrototype),
6060
AuthPrototype(AuthenticationPrototype),
6161
AttributePrototypeArgument(AttributePrototypeArgument),
@@ -68,7 +68,7 @@ enum Thing {
6868
Socket(Box<(Socket, Option<InternalProvider>, Option<ExternalProvider>)>),
6969
}
7070

71-
type ThingMap = super::ChangeSetThingMap<String, Thing>;
71+
pub type ThingMap = super::ChangeSetThingMap<String, Thing>;
7272

7373
#[derive(Clone, Debug, Default)]
7474
pub struct ImportOptions {
@@ -82,6 +82,8 @@ pub struct ImportOptions {
8282
pub is_builtin: bool,
8383
}
8484

85+
const SPECIAL_CASE_FUNCS: [&str; 2] = ["si:resourcePayloadToValue", "si:normalizeToArray"];
86+
8587
#[allow(clippy::too_many_arguments)]
8688
async fn import_change_set(
8789
ctx: &DalContext,
@@ -104,9 +106,8 @@ async fn import_change_set(
104106
// This is a hack because the hash of the intrinsics has changed from the version in the
105107
// packages. We also apply this to si:resourcePayloadToValue since it should be an
106108
// intrinsic but is only in our packages
107-
let special_case_funcs = ["si:resourcePayloadToValue", "si:normalizeToArray"];
108109
if func::is_intrinsic(func_spec.name())
109-
|| special_case_funcs.contains(&func_spec.name())
110+
|| SPECIAL_CASE_FUNCS.contains(&func_spec.name())
110111
|| func_spec.is_from_builtin().unwrap_or(false)
111112
{
112113
let hash = func_spec.hash();
@@ -1485,7 +1486,7 @@ async fn update_func(
14851486
Ok(())
14861487
}
14871488

1488-
async fn import_func(
1489+
pub async fn import_func(
14891490
ctx: &DalContext,
14901491
change_set_pk: ChangeSetPk,
14911492
func_spec: &FuncSpec,
@@ -2459,7 +2460,29 @@ async fn update_schema_variant(
24592460
Ok(())
24602461
}
24612462

2462-
async fn import_schema_variant(
2463+
/// Duplicate all the functions, and return a thing_map with them included, so
2464+
/// that we can import a standalone schema variant.
2465+
pub async fn clone_and_import_funcs(ctx: &DalContext, funcs: Vec<FuncSpec>) -> PkgResult<ThingMap> {
2466+
let mut thing_map = ThingMap::new();
2467+
2468+
for func_spec in funcs {
2469+
let func = if func::is_intrinsic(&func_spec.name)
2470+
|| SPECIAL_CASE_FUNCS.contains(&func_spec.name.as_str())
2471+
{
2472+
Func::find_by_name(ctx, &func_spec.name)
2473+
.await?
2474+
.ok_or(PkgError::MissingIntrinsicFunc(func_spec.name.to_owned()))?
2475+
} else {
2476+
create_func(ctx, &func_spec).await?
2477+
};
2478+
2479+
thing_map.insert(ChangeSetPk::NONE, func_spec.unique_id, Thing::Func(func));
2480+
}
2481+
2482+
Ok(thing_map)
2483+
}
2484+
2485+
pub async fn import_schema_variant(
24632486
ctx: &DalContext,
24642487
change_set_pk: ChangeSetPk,
24652488
schema: &mut Schema,

lib/dal/src/schema/variant/definition.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,9 +499,9 @@ impl SchemaVariantDefinitionJson {
499499
metadata: SchemaVariantDefinitionMetadataJson,
500500
identity_func_unique_id: &str,
501501
asset_func_spec_unique_id: &str,
502+
name: &str,
502503
) -> SchemaVariantDefinitionResult<SchemaVariantSpec> {
503504
let mut builder = SchemaVariantSpec::builder();
504-
let name = "v0";
505505
builder.name(name);
506506

507507
let mut data_builder = SchemaVariantSpecData::builder();

0 commit comments

Comments
 (0)