Merge remote-tracking branch 'origin/1.8.x' into feat-db-tests

This commit is contained in:
Jake Barnby
2026-01-16 20:50:00 +13:00
21 changed files with 230 additions and 199 deletions
+3
View File
@@ -78,6 +78,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
@@ -168,6 +169,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
@@ -189,6 +191,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizatio
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
+1 -1
View File
@@ -227,7 +227,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '12.0.1',
'version' => '13.0.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
+16 -2
View File
@@ -13454,6 +13454,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -13467,7 +13477,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -13482,7 +13494,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"team": {
+16 -2
View File
@@ -55191,6 +55191,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -55204,7 +55214,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -55219,7 +55231,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"bucket": {
+16 -2
View File
@@ -43244,6 +43244,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -43257,7 +43267,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -43272,7 +43284,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"bucket": {
+16 -2
View File
@@ -13393,6 +13393,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -13406,7 +13416,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -13421,7 +13433,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"team": {
+16 -2
View File
@@ -55019,6 +55019,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -55032,7 +55042,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -55047,7 +55059,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"bucket": {
+16 -2
View File
@@ -43169,6 +43169,16 @@
"description": "Total number of chunks uploaded",
"x-example": 17890,
"format": "int32"
},
"encryption": {
"type": "boolean",
"description": "Whether file contents are encrypted at rest.",
"x-example": true
},
"compression": {
"type": "string",
"description": "Compression algorithm used for the file. Will be one of none, [gzip](https:\/\/en.wikipedia.org\/wiki\/Gzip), or [zstd](https:\/\/en.wikipedia.org\/wiki\/Zstd).",
"x-example": "gzip"
}
},
"required": [
@@ -43182,7 +43192,9 @@
"mimeType",
"sizeOriginal",
"chunksTotal",
"chunksUploaded"
"chunksUploaded",
"encryption",
"compression"
],
"example": {
"$id": "5e5ea5c16897e",
@@ -43197,7 +43209,9 @@
"mimeType": "image\/png",
"sizeOriginal": 17890,
"chunksTotal": 17890,
"chunksUploaded": 17890
"chunksUploaded": 17890,
"encryption": true,
"compression": "gzip"
}
},
"bucket": {
+1
View File
@@ -227,6 +227,7 @@ App::post('/v1/projects')
if (!$sharedTablesV2) {
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDatabase(APP_DATABASE);
if ($sharedTables) {
$dbForProject
+3
View File
@@ -161,6 +161,9 @@ const ACTIVITY_TYPE_GUEST = 'guest';
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database name
const APP_DATABASE = 'appwrite';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
+6
View File
@@ -324,6 +324,12 @@ $register->set('pools', function () {
Config::setParam('pools-' . $key, $config);
}
$reconnectAttempts = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_ATTEMPTS', 5);
$reconnectSleep = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_SLEEP', 2);
$group->setReconnectAttempts($reconnectAttempts);
$group->setReconnectSleep($reconnectSleep);
return $group;
});
+4
View File
@@ -533,6 +533,7 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
@@ -563,6 +564,7 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authoriz
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setMetadata('host', \gethostname())
@@ -592,6 +594,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
$configure = (function (Database $database) use ($project, $dsn, $authorization) {
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
@@ -643,6 +646,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizatio
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
+2
View File
@@ -66,6 +66,7 @@ if (!function_exists('getConsoleDB')) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, getCache());
$database
->setDatabase(APP_DATABASE)
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', '_console');
@@ -122,6 +123,7 @@ if (!function_exists('getProjectDB')) {
}
$database
->setDatabase(APP_DATABASE)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId());
+4
View File
@@ -65,6 +65,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register,
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class)
@@ -118,6 +119,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
@@ -179,6 +181,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
@@ -198,6 +201,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
Generated
+21 -20
View File
@@ -3455,24 +3455,25 @@
},
{
"name": "utopia-php/abuse",
"version": "1.0.2",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828"
"reference": "15cd5dbefa4453e8a2d90649a7078e242966ac3f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828",
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/15cd5dbefa4453e8a2d90649a7078e242966ac3f",
"reference": "15cd5dbefa4453e8a2d90649a7078e242966ac3f",
"shasum": ""
},
"require": {
"appwrite/appwrite": "19.*",
"ext-curl": "*",
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/database": "*"
"utopia-php/database": "4.*"
},
"require-dev": {
"laravel/pint": "1.*",
@@ -3500,9 +3501,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/1.0.2"
"source": "https://github.com/utopia-php/abuse/tree/1.2.1"
},
"time": "2025-10-20T07:18:33+00:00"
"time": "2026-01-15T02:09:49+00:00"
},
{
"name": "utopia-php/analytics",
@@ -3898,16 +3899,16 @@
},
{
"name": "utopia-php/database",
"version": "4.5.1",
"version": "4.5.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "7b935bb09aeae8aeff5a28f6f2485cef1cc4d898"
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/7b935bb09aeae8aeff5a28f6f2485cef1cc4d898",
"reference": "7b935bb09aeae8aeff5a28f6f2485cef1cc4d898",
"url": "https://api.github.com/repos/utopia-php/database/zipball/8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
"shasum": ""
},
"require": {
@@ -3950,9 +3951,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/4.5.1"
"source": "https://github.com/utopia-php/database/tree/4.5.2"
},
"time": "2026-01-14T12:07:24+00:00"
"time": "2026-01-15T04:23:30+00:00"
},
{
"name": "utopia-php/detector",
@@ -5481,16 +5482,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.8.15",
"version": "1.8.16",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "a43e8ba5d539e48f0717df284dbd5dc1fb659d6b"
"reference": "e2963002b39f3aa24bd6bf61a78492e182397174"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a43e8ba5d539e48f0717df284dbd5dc1fb659d6b",
"reference": "a43e8ba5d539e48f0717df284dbd5dc1fb659d6b",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/e2963002b39f3aa24bd6bf61a78492e182397174",
"reference": "e2963002b39f3aa24bd6bf61a78492e182397174",
"shasum": ""
},
"require": {
@@ -5526,9 +5527,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.15"
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.16"
},
"time": "2026-01-14T10:42:32+00:00"
"time": "2026-01-15T08:16:15+00:00"
},
{
"name": "brianium/paratest",
@@ -9197,5 +9198,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.9.0"
"plugin-api-version": "2.6.0"
}
+29
View File
@@ -1,5 +1,34 @@
# Change Log
## 13.0.0
- Mark release as stable
- Feat: add pull sync on destruction of remote resources (+ confirmation)
- Fix: refine zod schema to check string size
- Validate using zod schema during push cli command
- Maintain order of keys in local config
## 13.0.0-rc.5
- Fix push all command not working correctly
## 13.0.0-rc.4
- Fix CLI ES module import issues
## 13.0.0-rc.3
- Add `Schema` class for programmatically pushing and pulling appwrite config
- Add client side db generation using `schema.db.generate()` command
## 13.0.0-rc.2
- Fixes a lot of typescript errors throughout the codebase
## 13.0.0-rc.1
- Migrates codebase from JavaScript to TypeScript
## 12.0.1
Fix type generation for `point`, `lineString` and `polygon` columns
+2 -1
View File
@@ -399,7 +399,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setDefaultHeaders([
'X-Appwrite-Response-Format' => '1.8.0',
])
->setExclude($language['exclude'] ?? []);
->setExclude($language['exclude'] ?? [])
->setTest(false);
// Make sure we have a clean slate.
// Otherwise, all files in this dir will be pushed,
+6 -1
View File
@@ -8,6 +8,7 @@ use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Response\Model\Execution;
use Exception;
use Executor\Executor;
@@ -661,7 +662,11 @@ class Functions extends Action
->trigger();
if (!empty($error)) {
throw new Exception($error, $errorCode);
throw new AppwriteException(
AppwriteException::GENERAL_SERVER_ERROR,
$error ?: 'Function execution failed with no error message',
$errorCode
);
}
}
}
+36 -164
View File
@@ -666,171 +666,16 @@ abstract class Format
public function getResponseEnumName(string $model, string $param): ?string
{
switch ($model) {
case 'attributeString':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeInteger':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeFloat':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeBoolean':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeEmail':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeEnum':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeIp':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeUrl':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeDatetime':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeRelationship':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributePoint':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributeLine':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'attributePolygon':
switch ($param) {
case 'status':
return 'AttributeStatus';
}
break;
case 'columnString':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnInteger':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnFloat':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnBoolean':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnEmail':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnEnum':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnIp':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnUrl':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnDatetime':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnRelationship':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnPoint':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnLine':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'columnPolygon':
switch ($param) {
case 'status':
return 'ColumnStatus';
}
break;
case 'healthStatus':
switch ($param) {
case 'status':
return 'HealthCheckStatus';
}
break;
if ($param !== 'status') {
return null;
}
return null;
return match (true) {
$model === 'healthStatus' => 'HealthCheckStatus',
str_starts_with($model, 'attribute') => 'AttributeStatus',
str_starts_with($model, 'column') => 'ColumnStatus',
default => null,
};
}
protected function getNestedModels(Model $model, array &$usedModels): void
@@ -852,4 +697,31 @@ abstract class Format
}
}
}
protected function parseDescription(string $description, array $excludedValues): string
{
if (empty($excludedValues)) {
return $description;
}
foreach ($excludedValues as $excludedValue) {
// remove from comma-separated list
$description = preg_replace(
'/,\s*' . preg_quote($excludedValue, '/') . '(?=\s*[,.]|$)/',
'',
$description
);
$description = preg_replace(
'/(?<=:\s|,\s)' . preg_quote($excludedValue, '/') . '\s*,\s*/',
'',
$description
);
}
// clean up double commas and extra spaces
$description = preg_replace('/,\s*,/', ',', $description);
$description = preg_replace('/\s+/', ' ', $description);
return trim($description);
}
}
@@ -623,6 +623,10 @@ class OpenAPI3 extends Format
$node['schema']['items']['enum'] = $enumValues;
$node['schema']['items']['x-enum-name'] = $this->getRequestEnumName($sdk->getNamespace() ?? '', $methodName, $name);
$node['schema']['items']['x-enum-keys'] = $enumKeys;
if (!empty($excludeKeys)) {
$node['description'] = $this->parseDescription($node['description'], $excludeKeys);
}
}
if ($validator->getType() === 'integer') {
$node['schema']['items']['format'] = $validator->getFormat() ?? 'int32';
@@ -673,6 +677,10 @@ class OpenAPI3 extends Format
$node['schema']['enum'] = $enumValues;
$node['schema']['x-enum-name'] = $this->getRequestEnumName($sdk->getNamespace() ?? '', $methodName, $name);
$node['schema']['x-enum-keys'] = $enumKeys;
if (!empty($excludeKeys)) {
$node['description'] = $this->parseDescription($node['description'], $excludeKeys);
}
}
if ($validator->getType() === 'integer') {
$node['schema']['format'] = $validator->getFormat() ?? 'int32';
@@ -607,6 +607,10 @@ class Swagger2 extends Format
$node['items']['enum'] = $enumValues;
$node['items']['x-enum-name'] = $this->getRequestEnumName($namespace, $methodName, $name);
$node['items']['x-enum-keys'] = $enumKeys;
if (!empty($excludeKeys)) {
$node['description'] = $this->parseDescription($node['description'], $excludeKeys);
}
}
if ($validator->getType() === 'integer') {
$node['items']['format'] = $validator->getFormat() ?? 'int32';
@@ -651,6 +655,10 @@ class Swagger2 extends Format
$node['enum'] = $enumValues;
$node['x-enum-name'] = $this->getRequestEnumName($namespace, $methodName, $name);
$node['x-enum-keys'] = $enumKeys;
if (!empty($excludeKeys)) {
$node['description'] = $this->parseDescription($node['description'], $excludeKeys);
}
}
if ($validator->getType() === 'integer') {
$node['format'] = $validator->getFormat() ?? 'int32';