diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue
index 124b5b8aeb..2685ddd8fc 100644
--- a/apps/desktop/src/components/grid/DataGrid.vue
+++ b/apps/desktop/src/components/grid/DataGrid.vue
@@ -7932,10 +7932,15 @@ function generateSelectionMenuItems(disabled: boolean): ContextMenuItem[] {
disabled,
},
{
- label: t("grid.generateUuid"),
+ label: t("grid.generateUuidV4"),
action: () => applyGeneratedSelectionValue("uuid"),
disabled,
},
+ {
+ label: t("grid.generateUuidV7"),
+ action: () => applyGeneratedSelectionValue("uuid-v7"),
+ disabled,
+ },
{
label: t("grid.generateSnowflakeId"),
action: () => applyGeneratedSelectionValue("snowflake"),
@@ -12817,7 +12822,8 @@ function openGridSnapshot() {
{{ t("grid.generateNull") }}
{{ t("grid.generateCurrentDatetime") }}
{{ t("grid.generateCurrentDate") }}
- {{ t("grid.generateUuid") }}
+ {{ t("grid.generateUuidV4") }}
+ {{ t("grid.generateUuidV7") }}
{{ t("grid.generateSnowflakeId") }}
{{ t("grid.generateIncrementId") }}
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts
index 6ae6fb62cf..49ec31d709 100644
--- a/apps/desktop/src/i18n/locales/az.ts
+++ b/apps/desktop/src/i18n/locales/az.ts
@@ -2027,7 +2027,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "Cari tarix və vaxt",
generateCurrentDate: "Cari tarix",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "Artan ID",
generateSnowflakeId: "Snowflake ID",
generateSequenceDescription: "Seçilmiş {count} xana üçün ardıcıl dəyərlər yaradın. Başlanğıc dəyəri daxil edin.",
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 654ec509cf..a58c09bccf 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -2028,7 +2028,8 @@ export default {
generateNull: "NULL",
generateCurrentDatetime: "Current Datetime",
generateCurrentDate: "Current Date",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "Increment ID",
generateSnowflakeId: "Snowflake ID",
generateSequenceDescription: "Generate consecutive values for {count} selected cell(s). Enter the start value.",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 79d90ff3aa..7557868701 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -1882,7 +1882,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "Fecha y hora actuales",
generateCurrentDate: "Fecha actual",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "ID incremental",
generateSnowflakeId: "ID Snowflake",
generateSequenceDescription: "Generar valores consecutivos para {count} celda(s) seleccionada(s). Introduzca el valor inicial.",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 177259a3a7..f2aae64a4a 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -1880,7 +1880,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "Data e ora correnti",
generateCurrentDate: "Data corrente",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "ID incrementale",
generateSnowflakeId: "ID Snowflake",
generateSequenceDescription: "Genera valori consecutivi per le {count} celle selezionate. Inserisci il valore iniziale.",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index cc3dbad6a8..87c0b7fdc8 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -1892,7 +1892,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "現在の日時",
generateCurrentDate: "現在の日付",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "連番ID",
generateSnowflakeId: "Snowflake ID",
generateSequenceDescription: "選択した{count}セルに連続値を生成します。開始値を入力してください。",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index f4e6ae4395..75f1e4720c 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -1864,7 +1864,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "현재 날짜/시간",
generateCurrentDate: "현재 날짜",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "증가 ID",
generateSnowflakeId: "Snowflake ID",
generateSequenceDescription: "선택한 {count}개 셀에 연속 값을 생성합니다. 시작 값을 입력하세요.",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 338f69b996..d76c7eaf95 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -1882,7 +1882,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "Data e hora atuais",
generateCurrentDate: "Data atual",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "ID incremental",
generateSnowflakeId: "ID Snowflake",
generateSequenceDescription: "Gere valores consecutivos para {count} célula(s) selecionada(s). Informe o valor inicial.",
diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts
index 4a65ed1cb8..4984ec4dc3 100644
--- a/apps/desktop/src/i18n/locales/tr.ts
+++ b/apps/desktop/src/i18n/locales/tr.ts
@@ -2022,7 +2022,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "Geçerli Tarih-Saat",
generateCurrentDate: "Geçerli Tarih",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "Artan Kimlik",
generateSnowflakeId: "Snowflake Kimliği",
generateSequenceDescription: "Seçili {count} hücre için ardışık değerler üretin. Başlangıç değerini girin.",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 9570b0842b..4577884bea 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1952,7 +1952,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "当前日期时间",
generateCurrentDate: "当前日期",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "递增 ID",
generateSnowflakeId: "雪花 ID",
generateSequenceDescription: "为已选 {count} 个单元格生成连续值,请输入起始值。",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 51614bf5af..ca31674d29 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -1881,7 +1881,8 @@ export default withEnglishFallback({
generateNull: "NULL",
generateCurrentDatetime: "目前日期時間",
generateCurrentDate: "目前日期",
- generateUuid: "UUID",
+ generateUuidV4: "UUID v4",
+ generateUuidV7: "UUID v7",
generateIncrementId: "遞增 ID",
generateSnowflakeId: "雪花 ID",
generateSequenceDescription: "為已選 {count} 個儲存格產生連續值,請輸入起始值。",
diff --git a/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts
index 93ff7e53bf..7891898a70 100644
--- a/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts
+++ b/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts
@@ -1,7 +1,11 @@
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import { createSnowflakeIdGenerator, generateCellValues } from "@/lib/dataGrid/cellValueGeneration";
describe("generateCellValues", () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
it("generates empty strings and nulls", () => {
expect(generateCellValues("empty", 2)).toEqual(["", ""]);
expect(generateCellValues("null", 2)).toEqual([null, null]);
@@ -18,6 +22,58 @@ describe("generateCellValues", () => {
expect(generateCellValues("uuid", 3, { uuidFactory: () => `uuid-${++index}` })).toEqual(["uuid-1", "uuid-2", "uuid-3"]);
});
+ it.each([
+ ["uuid", /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/],
+ ["uuid-v7", /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/],
+ ] as const)("generates distinct %s values for cells in the same millisecond", (kind, pattern) => {
+ const values = generateCellValues(kind, 5000, { now: new Date(1_645_557_742_000) });
+
+ expect(values).toHaveLength(5000);
+ expect(values.every((value) => typeof value === "string" && pattern.test(value))).toBe(true);
+ expect(new Set(values).size).toBe(values.length);
+ });
+
+ it("encodes the UUID v7 timestamp and random fields from the RFC 9562 test vector", () => {
+ const values = generateCellValues("uuid-v7", 1, {
+ now: new Date(1_645_557_742_000),
+ uuidFactory: () => "11111111-2222-4cc3-98c4-dc0c0c07398f",
+ });
+
+ expect(values).toEqual(["017f22e2-79b0-7cc3-98c4-dc0c0c07398f"]);
+ });
+
+ it("pads early UUID v7 timestamps to 48 bits", () => {
+ expect(
+ generateCellValues("uuid-v7", 1, {
+ now: new Date(1),
+ uuidFactory: () => "11111111-2222-4cc3-98c4-dc0c0c07398f",
+ }),
+ ).toEqual(["00000000-0001-7cc3-98c4-dc0c0c07398f"]);
+ });
+
+ it("orders UUID v7 values by millisecond across a timestamp field boundary", () => {
+ const [earlier] = generateCellValues("uuid-v7", 1, {
+ now: new Date(0xffff_ffff),
+ uuidFactory: () => "ffffffff-ffff-4fff-bfff-ffffffffffff",
+ });
+ const [later] = generateCellValues("uuid-v7", 1, {
+ now: new Date(0x1_0000_0000),
+ uuidFactory: () => "00000000-0000-4000-8000-000000000000",
+ });
+
+ expect(earlier).toBe("0000ffff-ffff-7fff-bfff-ffffffffffff");
+ expect(later).toBe("00010000-0000-7000-8000-000000000000");
+ expect(earlier! < later!).toBe(true);
+ });
+
+ it("generates UUID v7 values when crypto.randomUUID is unavailable", () => {
+ vi.stubGlobal("crypto", {});
+ const values = generateCellValues("uuid-v7", 20, { now: new Date(1_645_557_742_000) });
+
+ expect(values.every((value) => /^017f22e2-79b0-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value!))).toBe(true);
+ expect(new Set(values).size).toBe(20);
+ });
+
it("increments values without losing bigint precision", () => {
expect(generateCellValues("increment", 3, { startValue: 9_007_199_254_740_993n })).toEqual(["9007199254740993", "9007199254740994", "9007199254740995"]);
});
diff --git a/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts b/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts
index bcd76866fb..b833979f95 100644
--- a/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts
+++ b/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts
@@ -1,6 +1,6 @@
import { uuid } from "@/lib/common/utils";
-export type CellValueGenerationKind = "empty" | "null" | "datetime" | "date" | "uuid" | "increment" | "snowflake";
+export type CellValueGenerationKind = "empty" | "null" | "datetime" | "date" | "uuid" | "uuid-v7" | "increment" | "snowflake";
export interface SnowflakeIdGenerator {
next(nowMs?: number): string;
@@ -59,6 +59,12 @@ export function generateCellValues(
if (kind === "datetime") return localDateTimeText(now);
if (kind === "date") return localDateText(now);
if (kind === "uuid") return uuidFactory();
+ if (kind === "uuid-v7") {
+ // RFC 9562: 48-bit Unix milliseconds, version 7, then 74 random bits and the UUID variant.
+ // A v4 UUID already supplies the random bits and variant in the same positions.
+ const timestamp = now.getTime().toString(16).padStart(12, "0");
+ return `${timestamp.slice(0, 8)}-${timestamp.slice(8)}-7${uuidFactory().slice(15)}`;
+ }
if (kind === "increment") return String((options.startValue ?? 1n) + BigInt(index));
return snowflakeGenerator.next(now.getTime());
});