Create a Store global attribute with a SQL migration in Znode 9

Create a Store support-label field, connect its metadata to an existing group and family, and preserve administrator labels and Store values on reruns.

Znode 9RecipeIntermediateGCG engineering guide
Workflow walkthrough

Try the Store-setting experience

Change a fictional Store’s support label and see the display rules in action. This demonstrates the result; SQL and Razor run in your Znode environment.

Open interactive preview
Browser-only demo · example data
Follow the complete recipe
In this guide
  1. Store entity and configured family
  2. Configured attribute group
  3. GcgSupportLabel definition
  4. Localized Admin label
  5. Store value set in Admin
Metadata makes the field available. A Store value is a separate record, entered and verified through the application.

One field, several relationships

A support team wants a short label beside its Store contact information. The value belongs to a Store, so this recipe uses global-attribute metadata. A product specification belongs to PIM and follows a different set of tables. Decide that ownership before writing a migration.

The fictional attribute is GcgSupportLabel. Its definition says what the field is. A localized label names the field in Admin. A group places it on a form; a family and entity mapping connect the group to the intended Store configuration. None of those metadata rows is the value entered for a particular Store. This example deliberately leaves Store values under administrative control.

Prepare the target and the runner

Use Admin > Global Attributes > Attribute Groups to create or select the Store group, then associate it with the existing Store family under Global Attribute Families. Znode 9 maintains one family for each Store, User, or Account entity type. This example expects group code GcgContent and family code Store; substitute the existing family code in your installation. Confirm their locale labels before deployment.

Set @AuditUserId to an existing active deployment actor and review the locale code. IDs are resolved from bounded codes for the other prerequisites. The script rejects missing or ambiguous lookups. It also selects the non-PIM Text type explicitly, because a type name alone is not enough context.

Use one dedicated deployment connection. This example owns its transaction and refuses an ambient transaction. If your DbUp runner owns transactions, adapt the transaction boundary as a reviewed change before using it. A transaction-scoped application lock serializes other deployments that use the same lock name. It does not stop Admin or unrelated scripts from writing these tables, so use a controlled deployment window.

Add the metadata migration

Save this as a new numbered SQL migration. Before execution, set @AuditUserId and confirm the locale, group, family, and attribute codes. The script creates the Store field definition and missing relationships; it does not assign a value to any Store.

Read it in three passes: validate the deployment actor and prerequisites, create or verify the field definition, then add missing label and form mappings. Each lookup must resolve one intended row. Existing labels are preserved, while conflicting definitions or duplicate rows stop the migration.

001-add-support-label.sql sql

-- Original GCG example. Znode 9 global-attribute schema pattern.
-- Configure these inputs after checking the target database and patch.
-- Run on a dedicated deployment connection without an outer transaction.
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @AuditUserId int = NULL; -- Required: existing deployment actor.
DECLARE @LocaleCode nvarchar(50) = N'en-US';
DECLARE @GroupCode varchar(200) = 'GcgContent';
DECLARE @FamilyCode varchar(200) = 'Store';
DECLARE @Code nvarchar(300) = N'GcgSupportLabel';
DECLARE @Label nvarchar(300) = N'Support label';
DECLARE @EntityId int, @TypeId int, @LocaleId int,
        @GroupId int, @FamilyId int, @AttributeId int,
        @LockResult int, @Now datetime = GETDATE();

IF @@TRANCOUNT <> 0
    THROW 51000, 'This example owns its transaction. Review runner configuration.', 1;
IF @AuditUserId IS NULL OR NOT EXISTS
   (SELECT 1 FROM dbo.ZnodeUser WHERE UserId = @AuditUserId AND IsActive = 1)
    THROW 51001, 'Set an existing active deployment actor.', 1;

BEGIN TRY
    BEGIN TRANSACTION;
    EXEC @LockResult = sys.sp_getapplock
        @Resource = N'GCG.StoreAttributeMetadata',
        @LockMode = 'Exclusive', @LockOwner = 'Transaction',
        @LockTimeout = 10000;
    IF @LockResult < 0
        THROW 51002, 'Another cooperating attribute deployment is running.', 1;

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalEntity
        WHERE EntityName = N'Store' AND IsActive = 1) <> 1
        THROW 51003, 'Expected exactly one active Store entity.', 1;
    SELECT @EntityId = GlobalEntityId FROM dbo.ZnodeGlobalEntity
    WHERE EntityName = N'Store' AND IsActive = 1;

    IF (SELECT COUNT(*) FROM dbo.ZnodeAttributeType
        WHERE AttributeTypeName = 'Text' AND IsPimAttributeType = 0) <> 1
        THROW 51004, 'Expected exactly one non-PIM Text type.', 1;
    SELECT @TypeId = AttributeTypeId FROM dbo.ZnodeAttributeType
    WHERE AttributeTypeName = 'Text' AND IsPimAttributeType = 0;

    IF (SELECT COUNT(*) FROM dbo.ZnodeLocale
        WHERE Code = @LocaleCode AND IsActive = 1) <> 1
        THROW 51005, 'Expected exactly one active locale.', 1;
    SELECT @LocaleId = LocaleId FROM dbo.ZnodeLocale
    WHERE Code = @LocaleCode AND IsActive = 1;

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalAttributeGroup
        WHERE GroupCode = @GroupCode AND GlobalEntityId = @EntityId) <> 1
        THROW 51006, 'Create or select one Store group before deployment.', 1;
    SELECT @GroupId = GlobalAttributeGroupId FROM dbo.ZnodeGlobalAttributeGroup
    WHERE GroupCode = @GroupCode AND GlobalEntityId = @EntityId;
    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalAttributeFamily
        WHERE FamilyCode = @FamilyCode AND GlobalEntityId = @EntityId) <> 1
        THROW 51007, 'Expected exactly one intended Store family.', 1;
    SELECT @FamilyId = GlobalAttributeFamilyId FROM dbo.ZnodeGlobalAttributeFamily
    WHERE FamilyCode = @FamilyCode AND GlobalEntityId = @EntityId;

    IF EXISTS (SELECT 1 FROM dbo.ZnodeGlobalAttribute
               WHERE AttributeCode = @Code
                 AND (GlobalEntityId <> @EntityId OR GlobalEntityId IS NULL))
        THROW 51014, 'Attribute code is already used by another entity.', 1;

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalAttribute
        WHERE AttributeCode = @Code AND GlobalEntityId = @EntityId) > 1
        THROW 51008, 'Duplicate attribute definitions require investigation.', 1;
    SELECT @AttributeId = GlobalAttributeId FROM dbo.ZnodeGlobalAttribute
    WHERE AttributeCode = @Code AND GlobalEntityId = @EntityId;
    IF @AttributeId IS NOT NULL AND EXISTS
       (SELECT 1 FROM dbo.ZnodeGlobalAttribute WHERE GlobalAttributeId = @AttributeId
        AND (AttributeTypeId IS NULL OR AttributeTypeId <> @TypeId
          OR ISNULL(IsSystemDefined, 1) <> 0 OR ISNULL(IsRequired, 1) <> 0
          OR ISNULL(IsLocalizable, 0) <> 1 OR ISNULL(IsActive, 0) <> 1))
        THROW 51009, 'Existing definition conflicts with this migration.', 1;

    IF @AttributeId IS NULL
    BEGIN
        INSERT dbo.ZnodeGlobalAttribute
          (AttributeTypeId, AttributeCode, IsRequired, IsLocalizable, IsActive,
           DisplayOrder, HelpDescription, CreatedBy, CreatedDate,
           ModifiedBy, ModifiedDate, IsSystemDefined, GlobalEntityId)
        VALUES (@TypeId, @Code, 0, 1, 1, 100, N'Customer-facing support label',
                @AuditUserId, @Now, @AuditUserId, @Now, 0, @EntityId);
        SET @AttributeId = CONVERT(int, SCOPE_IDENTITY());
    END;

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalAttributeLocale
        WHERE GlobalAttributeId = @AttributeId AND LocaleId = @LocaleId) > 1
        THROW 51010, 'Duplicate localized labels require investigation.', 1;
    IF NOT EXISTS (SELECT 1 FROM dbo.ZnodeGlobalAttributeLocale
                   WHERE GlobalAttributeId = @AttributeId AND LocaleId = @LocaleId)
        INSERT dbo.ZnodeGlobalAttributeLocale
          (LocaleId, GlobalAttributeId, AttributeName, Description,
           CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
        VALUES (@LocaleId, @AttributeId, @Label, NULL,
                @AuditUserId, @Now, @AuditUserId, @Now);
    -- An existing translated label belongs to the administrator. Preserve it.

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalAttributeGroupMapper
        WHERE GlobalAttributeGroupId = @GroupId AND GlobalAttributeId = @AttributeId) > 1
        THROW 51011, 'Duplicate attribute/group mapping.', 1;
    IF NOT EXISTS (SELECT 1 FROM dbo.ZnodeGlobalAttributeGroupMapper
        WHERE GlobalAttributeGroupId = @GroupId AND GlobalAttributeId = @AttributeId)
        INSERT dbo.ZnodeGlobalAttributeGroupMapper
          (GlobalAttributeGroupId, GlobalAttributeId, AttributeDisplayOrder,
           CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
        VALUES (@GroupId, @AttributeId, 100, @AuditUserId, @Now, @AuditUserId, @Now);

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalFamilyGroupMapper
        WHERE GlobalAttributeFamilyId = @FamilyId AND GlobalAttributeGroupId = @GroupId) > 1
        THROW 51012, 'Duplicate family/group mapping.', 1;
    IF NOT EXISTS (SELECT 1 FROM dbo.ZnodeGlobalFamilyGroupMapper
        WHERE GlobalAttributeFamilyId = @FamilyId AND GlobalAttributeGroupId = @GroupId)
        INSERT dbo.ZnodeGlobalFamilyGroupMapper
          (GlobalAttributeFamilyId, GlobalAttributeGroupId, AttributeGroupDisplayOrder,
           CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
        VALUES (@FamilyId, @GroupId, 100, @AuditUserId, @Now, @AuditUserId, @Now);

    IF (SELECT COUNT(*) FROM dbo.ZnodeGlobalGroupEntityMapper
        WHERE GlobalAttributeGroupId = @GroupId AND GlobalEntityId = @EntityId) > 1
        THROW 51013, 'Duplicate group/entity mapping.', 1;
    IF NOT EXISTS (SELECT 1 FROM dbo.ZnodeGlobalGroupEntityMapper
        WHERE GlobalAttributeGroupId = @GroupId AND GlobalEntityId = @EntityId)
        INSERT dbo.ZnodeGlobalGroupEntityMapper
          (GlobalAttributeGroupId, GlobalEntityId, AttributeGroupDisplayOrder,
           CreatedBy, CreatedDate, ModifiedBy, ModifiedDate)
        VALUES (@GroupId, @EntityId, 100, @AuditUserId, @Now, @AuditUserId, @Now);
    COMMIT TRANSACTION;
    SELECT @Code AS AttributeCode, @AttributeId AS AttributeId,
           @GroupCode AS GroupCode, @FamilyCode AS FamilyCode, @LocaleCode AS LocaleCode;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;

On a fresh run, GcgSupportLabel is created as an active, optional, localizable Text field under GcgContent and the Store family. On an unchanged rerun, the same metadata is reused. If an administrator has renamed the displayed label, that label remains. A failed step rolls back this script's transaction; its transaction-scoped application lock coordinates only deployments using the same lock name.

Read back the graph

Run this query with the same codes used by the migration. It checks the exact path from Store entity to attribute, locale label, group, and family. This recipe expects one row with GcgSupportLabel, the selected label, en-US, GcgContent, and Store.

Zero rows means at least one required relationship is absent. More than one row requires investigation of the individual mappings, because joins can multiply duplicate relationships. Use the missing-mapping diagnostic guide to locate an absent relationship before preparing a correction.

verify-support-label.sql sql

SELECT a.AttributeCode, l.AttributeName, loc.Code AS LocaleCode,
       g.GroupCode, f.FamilyCode, e.EntityName
FROM dbo.ZnodeGlobalAttribute a
JOIN dbo.ZnodeGlobalEntity e ON e.GlobalEntityId = a.GlobalEntityId
JOIN dbo.ZnodeGlobalAttributeLocale l ON l.GlobalAttributeId = a.GlobalAttributeId
JOIN dbo.ZnodeLocale loc ON loc.LocaleId = l.LocaleId
JOIN dbo.ZnodeGlobalAttributeGroupMapper gm ON gm.GlobalAttributeId = a.GlobalAttributeId
JOIN dbo.ZnodeGlobalAttributeGroup g ON g.GlobalAttributeGroupId = gm.GlobalAttributeGroupId
JOIN dbo.ZnodeGlobalFamilyGroupMapper fm ON fm.GlobalAttributeGroupId = g.GlobalAttributeGroupId
JOIN dbo.ZnodeGlobalAttributeFamily f ON f.GlobalAttributeFamilyId = fm.GlobalAttributeFamilyId
JOIN dbo.ZnodeGlobalGroupEntityMapper em ON em.GlobalAttributeGroupId = g.GlobalAttributeGroupId
  AND em.GlobalEntityId = e.GlobalEntityId
WHERE a.AttributeCode = N'GcgSupportLabel' AND e.EntityName = N'Store'
  AND g.GroupCode = 'GcgContent' AND f.FamilyCode = 'Store'
  AND g.GlobalEntityId = e.GlobalEntityId AND f.GlobalEntityId = e.GlobalEntityId
  AND loc.Code = N'en-US';

The query verifies metadata, not the value for a particular Store. Finish by entering a sample value in Additional Store Attributes, reopening it in the same locale, and reading it through the application's Store attribute service.

Verify the Store workflow

Open Stores and Reps > Stores > Additional Store Attributes and locate the field for the intended Store. Enter a synthetic support label, save, reload, and verify the same locale. Read the value through the application path used by your customization. Publish Store settings and refresh caches when required by that consuming path and Znode patch.

Exercise fresh creation, an unchanged rerun, repair of a missing mapping, an edited Admin label, a conflicting definition, and duplicate locale rows in a disposable copy of the target schema. Verify the same field through Admin, the application read path, and the deployment runner. These checks connect migration behavior to the actual Store configuration.

  • Run against a disposable copy of the target schema before release.
  • Record the exact Znode patch, SQL Server version, migration runner, and observed Admin result.
  • Test missing group/family prerequisites and a same-code conflict in your target schema.
  • Verify no Store values or unrelated metadata changed.

Make the next change a forward migration

Once the file has been recorded in the migration journal, editing that file is not a reliable way to update deployed environments. Add a new migration for a new requirement. Keep deliberate value changes separate from metadata creation so reviewers can see exactly which administrator-owned configuration is being changed.

Avoid deleting the attribute as a routine rollback once users have entered data or customizations depend on it. A forward correction is often safer. If removal is required, first inventory its values and relationships and define a reviewed recovery path. The next recipe explains the difference between journaled migrations and programmability files that run on every deployment.

Inspect exact column types before preparing the migration

Run this read-only SQL Server catalog query in the intended Znode 9 database with metadata visibility. Compare the reported type, size, nullability, identity, and default for each column against the migration inputs. MaximumBytes is storage bytes: a positive nvarchar value uses two bytes per UTF-16 storage unit, while -1 means max. This check describes columns; it does not establish the complete Admin or application behavior.

inspect-global-attribute-columns.sql sql

SELECT s.name AS SchemaName, t.name AS TableName,
       c.column_id, c.name AS ColumnName, ty.name AS TypeName,
       c.max_length AS MaximumBytes, c.precision, c.scale,
       c.is_nullable, c.is_identity, dc.definition AS DefaultExpression
FROM sys.tables AS t
JOIN sys.schemas AS s ON s.schema_id = t.schema_id
JOIN sys.columns AS c ON c.object_id = t.object_id
JOIN sys.types AS ty ON ty.user_type_id = c.user_type_id
LEFT JOIN sys.default_constraints AS dc
  ON dc.object_id = c.default_object_id
WHERE s.name = N'dbo'
  AND t.name IN (
    N'ZnodeGlobalAttribute', N'ZnodeGlobalAttributeLocale',
    N'ZnodeGlobalAttributeGroupMapper', N'ZnodeGlobalFamilyGroupMapper',
    N'ZnodeGlobalGroupEntityMapper')
ORDER BY t.name, c.column_id;

References and further reading

Bring your next engineering question.

GCG helps teams review attribute migrations, deployment behavior, and the application paths that depend on them.

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Fix out-of-order cart updates before they corrupt the UI

Next
Next

Plan a Znode 9 product attribute migration without confusing it with a Store attribute