diff --git a/.scoper-production-dependencies b/.scoper-production-dependencies
index e29620a89e..336650b97b 100644
--- a/.scoper-production-dependencies
+++ b/.scoper-production-dependencies
@@ -7,3 +7,5 @@ psr/http-client
psr/http-factory
psr/http-message
psr/simple-cache
+symfony/uid
+symfony/polyfill-uuid
diff --git a/appinfo/info.xml b/appinfo/info.xml
index e1f28be62a..489ba0fd63 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -26,7 +26,7 @@ Share your tables and views with users and groups within your cloud.
Have a good time and manage whatever you want.
]]>
- 2.1.1
+ 2.2.0-dev.0
AGPL-3.0-or-later
Florian Steffens
Tables
diff --git a/composer.json b/composer.json
index 489681ad53..059ed891e5 100644
--- a/composer.json
+++ b/composer.json
@@ -66,6 +66,7 @@
"require": {
"phpoffice/phpspreadsheet": "^5.1",
"ext-json": "*",
- "bamarni/composer-bin-plugin": "^1.9.1"
+ "bamarni/composer-bin-plugin": "^1.9.1",
+ "symfony/uid": "^6.4"
}
}
diff --git a/composer.lock b/composer.lock
index 27c9e8da40..e404bb10a3 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b78c507d2c52ea02251a257651916b49",
+ "content-hash": "a23ff2aecc750aa400068e3f79dc35b8",
"packages": [
{
"name": "bamarni/composer-bin-plugin",
@@ -642,6 +642,167 @@
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
"time": "2021-10-29T13:26:27+00:00"
+ },
+ {
+ "name": "symfony/polyfill-uuid",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-uuid.git",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-uuid": "*"
+ },
+ "suggest": {
+ "ext-uuid": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Uuid\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for uuid functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/uid",
+ "version": "v6.4.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/uid.git",
+ "reference": "6b973c385f00341b246f697d82dc01a09107acdd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/6b973c385f00341b246f697d82dc01a09107acdd",
+ "reference": "6b973c385f00341b246f697d82dc01a09107acdd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-uuid": "^1.15"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Uid\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to generate and represent UIDs",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "UID",
+ "ulid",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/uid/tree/v6.4.32"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-23T15:07:59+00:00"
}
],
"packages-dev": [
diff --git a/lib/Db/Column.php b/lib/Db/Column.php
index 7ebcdacf35..9ced1ff698 100644
--- a/lib/Db/Column.php
+++ b/lib/Db/Column.php
@@ -13,6 +13,7 @@
use OCA\Tables\Dto\Column as ColumnDto;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ValueObject\ViewColumnInformation;
+use OCA\Tables\Vendor\Symfony\Component\Uid\Uuid;
use ValueError;
/**
@@ -20,6 +21,7 @@
*
* @psalm-import-type TablesColumn from ResponseDefinitions
*
+ * @method string getUuid()
* @method getTitle(): string
* @method setTitle(string $title)
* @method getTableId(): int
@@ -59,13 +61,11 @@
* @method setTextDefault(?string $textDefault)
* @method getTextAllowedPattern(): string
* @method setTextAllowedPattern(?string $textAllowedPattern)
- * @method getTextAllowedPattern(): ?string
* @method getTextMaxLength(): int
* @method setTextMaxLength(?int $textMaxLength)
* @method getTextUnique(): bool
* @method setTextUnique(?bool $textUnique)
* @method getSelectionOptions(): string
- * @method getSelectionDefault(): string
* @method setSelectionOptions(?string $selectionOptionsArray)
* @method setSelectionDefault(?string $selectionDefault)
* @method getSelectionDefault(): ?string
@@ -114,6 +114,7 @@ class Column extends EntitySuper implements JsonSerializable {
public const META_ID_TITLE = 'id';
+ protected ?string $uuid = null;
protected ?string $title = null;
protected ?int $tableId = null;
protected ?string $createdBy = null;
@@ -165,6 +166,7 @@ class Column extends EntitySuper implements JsonSerializable {
public function __construct() {
$this->addType('id', 'integer');
+ $this->addType('uuid', 'string');
$this->addType('tableId', 'integer');
$this->addType('mandatory', 'boolean');
@@ -198,8 +200,23 @@ public static function isValidMetaTypeId(int $metaTypeId): bool {
], true);
}
+ private function assignUuid(): void {
+ if ($this->uuid !== null) {
+ throw new \RuntimeException('This column already has a UUID, they are immutable');
+ }
+ $this->uuid = Uuid::v7()->toRfc4122();
+ }
+
+ protected function setUuid(string $uuid): void {
+ if ($this->uuid !== null) {
+ throw new \RuntimeException('This column already has a UUID, they are immutable');
+ }
+ $this->uuid = $uuid;
+ }
+
public static function fromDto(ColumnDto $data): self {
$column = new self();
+ $column->assignUuid();
$column->setTitle($data->getTitle());
$column->setType($data->getType());
$column->setSubtype($data->getSubtype() ?? '');
@@ -262,6 +279,7 @@ public function setSelectionOptionsArray(array $array):void {
public function jsonSerialize(): array {
return [
'id' => $this->id,
+ 'uuid' => $this->uuid,
'tableId' => $this->tableId,
'title' => $this->title,
'createdBy' => $this->createdBy,
diff --git a/lib/Migration/Version2020Date20260513185340.php b/lib/Migration/Version2020Date20260513185340.php
new file mode 100644
index 0000000000..32766789b2
--- /dev/null
+++ b/lib/Migration/Version2020Date20260513185340.php
@@ -0,0 +1,96 @@
+hasTable(self::TARGET_TABLE)) {
+ return null;
+ }
+
+ $columnsTable = $schema->getTable(self::TARGET_TABLE);
+ if (!$columnsTable->hasColumn(self::COL_UUID)) {
+ $columnsTable->addColumn(self::COL_UUID, Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ 'length' => 36,
+ 'comment' => 'UUIDv7 identifier to support structural updates across instances',
+ ]);
+ }
+
+ return $schema;
+ }
+
+ #[Override]
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+
+ $qbUpdate = $this->db->getQueryBuilder();
+ $qbUpdate->update(self::TARGET_TABLE)
+ ->set(self::COL_UUID, $qbUpdate->createParameter('columnUuid'))
+ ->where($qbUpdate->expr()->eq(self::COL_ID, $qbUpdate->createParameter('columnLocalId')));
+
+ $qbSelect = $this->db->getQueryBuilder();
+ $qbSelect->select(self::COL_ID)
+ ->from(self::TARGET_TABLE);
+ $select = $qbSelect->executeQuery();
+
+ $writeBatches = 250;
+ $updates = 0;
+
+ try {
+ $this->db->beginTransaction();
+ while (($columnId = $select->fetchOne()) !== false) {
+ $qbUpdate->setParameters(
+ [
+ 'columnLocalId' => (int)$columnId,
+ 'columnUuid' => Uuid::v7()->toRfc4122(),
+ ],
+ [
+ Types::INTEGER,
+ Types::STRING,
+ ]
+ );
+ $qbUpdate->executeStatement();
+ $updates++;
+ if ($updates % $writeBatches === 0) {
+ $this->db->commit();
+ $this->db->beginTransaction();
+ }
+ }
+ $this->db->commit();
+ } catch (\Exception $e) {
+ $this->db->rollBack();
+ throw $e;
+ }
+
+ $select->closeCursor();
+ }
+}