@@ -1330,6 +1330,239 @@ public function updateDocument(string $collection, string $id, Document $documen
13301330 return $ document ;
13311331 }
13321332
1333+ /**
1334+ * @param string $collection
1335+ * @param string $attribute
1336+ * @param array<Document> $documents
1337+ * @return array<Document>
1338+ * @throws DatabaseException
1339+ */
1340+ public function createOrUpdateDocuments (
1341+ string $ collection ,
1342+ string $ attribute ,
1343+ array $ documents
1344+ ): array {
1345+ if (empty ($ documents )) {
1346+ return $ documents ;
1347+ }
1348+
1349+ try {
1350+ $ name = $ this ->filter ($ collection );
1351+ $ attribute = $ this ->filter ($ attribute );
1352+
1353+ $ attributes = [];
1354+ $ bindIndex = 0 ;
1355+ $ batchKeys = [];
1356+ $ bindValues = [];
1357+ $ documentIds = [];
1358+ $ documentTenants = [];
1359+
1360+ foreach ($ documents as $ document ) {
1361+ $ attributes = $ document ->getAttributes ();
1362+ $ attributes ['_uid ' ] = $ document ->getId ();
1363+ $ attributes ['_createdAt ' ] = $ document ->getCreatedAt ();
1364+ $ attributes ['_updatedAt ' ] = $ document ->getUpdatedAt ();
1365+ $ attributes ['_permissions ' ] = \json_encode ($ document ->getPermissions ());
1366+
1367+ if (!empty ($ document ->getInternalId ())) {
1368+ $ attributes ['_id ' ] = $ document ->getInternalId ();
1369+ } else {
1370+ $ documentIds [] = $ document ->getId ();
1371+ }
1372+
1373+ if ($ this ->sharedTables ) {
1374+ $ attributes ['_tenant ' ]
1375+ = $ documentTenants []
1376+ = $ document ->getTenant ();
1377+ }
1378+
1379+ $ columns = [];
1380+ foreach (\array_keys ($ attributes ) as $ key => $ attr ) {
1381+ $ columns [$ key ] = "{$ this ->quote ($ this ->filter ($ attr ))}" ;
1382+ }
1383+ $ columns = '( ' . \implode (', ' , $ columns ) . ') ' ;
1384+
1385+ $ bindKeys = [];
1386+
1387+ foreach ($ attributes as $ attrValue ) {
1388+ if (\is_array ($ attrValue )) {
1389+ $ attrValue = \json_encode ($ attrValue );
1390+ }
1391+ $ attrValue = (\is_bool ($ attrValue )) ? (int )$ attrValue : $ attrValue ;
1392+ $ bindKey = 'key_ ' . $ bindIndex ;
1393+ $ bindKeys [] = ': ' . $ bindKey ;
1394+ $ bindValues [$ bindKey ] = $ attrValue ;
1395+ $ bindIndex ++;
1396+ }
1397+
1398+ $ batchKeys [] = '( ' . \implode (', ' , $ bindKeys ) . ') ' ;
1399+ }
1400+
1401+ $ getUpdateClause = function (string $ attribute , bool $ increment = false ): string {
1402+ $ attribute = $ this ->quote ($ this ->filter ($ attribute ));
1403+
1404+ if ($ increment ) {
1405+ $ new = "{$ attribute } + VALUES( {$ attribute }) " ;
1406+ } else {
1407+ $ new = "VALUES( {$ attribute }) " ;
1408+ }
1409+
1410+ if ($ this ->sharedTables ) {
1411+ return "{$ attribute } = IF(_tenant = VALUES(_tenant), {$ new }, {$ attribute }) " ;
1412+ }
1413+
1414+ return "{$ attribute } = {$ new }" ;
1415+ };
1416+
1417+ if (!empty ($ attribute )) {
1418+ // Increment specific column by its new value in place
1419+ $ updateColumns = [
1420+ $ getUpdateClause ($ attribute , increment: true ),
1421+ $ getUpdateClause ('_updatedAt ' ),
1422+ ];
1423+ } else {
1424+ // Update all columns
1425+ $ updateColumns = [];
1426+ foreach (\array_keys ($ attributes ) as $ attr ) {
1427+ $ updateColumns [] = $ getUpdateClause ($ this ->filter ($ attr ));
1428+ }
1429+ }
1430+
1431+ $ stmt = $ this ->getPDO ()->prepare (
1432+ "
1433+ INSERT INTO {$ this ->getSQLTable ($ name )} {$ columns }
1434+ VALUES " . \implode (', ' , $ batchKeys ) . "
1435+ ON DUPLICATE KEY UPDATE
1436+ " . \implode (', ' , $ updateColumns )
1437+ );
1438+
1439+ foreach ($ bindValues as $ key => $ binding ) {
1440+ $ stmt ->bindValue ($ key , $ binding , $ this ->getPDOType ($ binding ));
1441+ }
1442+
1443+ $ stmt ->execute ();
1444+
1445+ // Fetch existing permissions in bulk after data updates
1446+ $ sql = "
1447+ SELECT _document, _type, _permission
1448+ FROM {$ this ->getSQLTable ($ name . '_perms ' )}
1449+ WHERE _document IN ( " . \implode (', ' , \array_map (fn ($ index ) => ":_key_ {$ index }" , \array_keys ($ documents ))) . ")
1450+ {$ this ->getTenantQuery ($ collection , tenantCount: \count ($ documentTenants ))}
1451+ " ;
1452+
1453+ $ stmt = $ this ->getPDO ()->prepare ($ sql );
1454+
1455+ foreach ($ documents as $ index => $ document ) {
1456+ $ stmt ->bindValue (":_key_ {$ index }" , $ document ->getId ());
1457+ }
1458+
1459+ if ($ this ->sharedTables ) {
1460+ foreach ($ documentTenants as $ index => $ tenant ) {
1461+ $ stmt ->bindValue (":_tenant_ {$ index }" , $ tenant );
1462+ }
1463+ }
1464+
1465+ $ stmt ->execute ();
1466+ $ existing = $ stmt ->fetchAll ();
1467+ $ stmt ->closeCursor ();
1468+
1469+ // Group permissions by document
1470+ $ permissionsByDocument = [];
1471+ foreach ($ existing as $ row ) {
1472+ $ permissionsByDocument [$ row ['_document ' ]][$ row ['_type ' ]][] = $ row ['_permission ' ];
1473+ }
1474+
1475+ foreach ($ documentIds as $ id ) {
1476+ foreach (Database::PERMISSIONS as $ type ) {
1477+ $ permissionsByDocument [$ id ][$ type ] = $ permissionsByDocument [$ id ][$ type ] ?? [];
1478+ }
1479+ }
1480+
1481+ $ removeQueries = [];
1482+ $ removeBindValues = [];
1483+ $ addQueries = [];
1484+ $ addBindValues = [];
1485+
1486+ foreach ($ documents as $ index => $ document ) {
1487+ $ currentPermissions = $ permissionsByDocument [$ document ->getId ()] ?? [];
1488+
1489+ // Calculate removals
1490+ foreach (Database::PERMISSIONS as $ type ) {
1491+ $ toRemove = \array_diff ($ currentPermissions [$ type ] ?? [], $ document ->getPermissionsByType ($ type ));
1492+ if (!empty ($ toRemove )) {
1493+ $ removeQueries [] = "(
1494+ _document = :_uid_ {$ index }
1495+ {$ this ->getTenantQuery ($ collection , tenantCount: \count ($ toRemove ))}
1496+ AND _type = ' {$ type }'
1497+ AND _permission IN ( " . \implode (', ' , \array_map (fn ($ i ) => ":remove_ {$ type }_ {$ index }_ {$ i }" , \array_keys ($ toRemove ))) . ")
1498+ ) " ;
1499+ $ removeBindValues [":_uid_ {$ index }" ] = $ document ->getId ();
1500+ $ removeBindValues [":_tenant_ {$ index }" ] = $ document ->getTenant ();
1501+ foreach ($ toRemove as $ i => $ perm ) {
1502+ $ removeBindValues [":remove_ {$ type }_ {$ index }_ {$ i }" ] = $ perm ;
1503+ }
1504+ }
1505+ }
1506+
1507+ // Calculate additions
1508+ foreach (Database::PERMISSIONS as $ type ) {
1509+ $ toAdd = \array_diff ($ document ->getPermissionsByType ($ type ), $ currentPermissions [$ type ] ?? []);
1510+ foreach ($ toAdd as $ i => $ permission ) {
1511+ $ addQuery = "(:_uid_ {$ index }, ' {$ type }', :add_ {$ type }_ {$ index }_ {$ i }" ;
1512+
1513+ if ($ this ->sharedTables ) {
1514+ $ addQuery .= ", :_tenant_ {$ index }" ;
1515+ }
1516+
1517+ $ addQuery .= ") " ;
1518+ $ addQueries [] = $ addQuery ;
1519+ $ addBindValues [":_uid_ {$ index }" ] = $ document ->getId ();
1520+ $ addBindValues [":add_ {$ type }_ {$ index }_ {$ i }" ] = $ permission ;
1521+
1522+ if ($ this ->sharedTables ) {
1523+ $ addBindValues [":_tenant_ {$ index }" ] = $ document ->getTenant ();
1524+ }
1525+ }
1526+ }
1527+ }
1528+
1529+ // Execute permission removals
1530+ if (!empty ($ removeQueries )) {
1531+ $ removeQuery = \implode (' OR ' , $ removeQueries );
1532+ $ stmtRemovePermissions = $ this ->getPDO ()->prepare ("DELETE FROM {$ this ->getSQLTable ($ name . '_perms ' )} WHERE {$ removeQuery }" );
1533+ foreach ($ removeBindValues as $ key => $ value ) {
1534+ $ stmtRemovePermissions ->bindValue ($ key , $ value , $ this ->getPDOType ($ value ));
1535+ }
1536+ $ stmtRemovePermissions ->execute ();
1537+ }
1538+
1539+ // Execute permission additions
1540+ if (!empty ($ addQueries )) {
1541+ $ sqlAddPermissions = "INSERT INTO {$ this ->getSQLTable ($ name . '_perms ' )} (_document, _type, _permission " ;
1542+ if ($ this ->sharedTables ) {
1543+ $ sqlAddPermissions .= ", _tenant " ;
1544+ }
1545+ $ sqlAddPermissions .= ") VALUES " . \implode (', ' , $ addQueries );
1546+ $ stmtAddPermissions = $ this ->getPDO ()->prepare ($ sqlAddPermissions );
1547+ foreach ($ addBindValues as $ key => $ value ) {
1548+ $ stmtAddPermissions ->bindValue ($ key , $ value , $ this ->getPDOType ($ value ));
1549+ }
1550+ $ stmtAddPermissions ->execute ();
1551+ }
1552+
1553+ $ internalIds = $ this ->getInternalIds ($ collection , $ documentIds , $ documentTenants );
1554+ foreach ($ documents as $ document ) {
1555+ if (isset ($ internalIds [$ document ->getId ()])) {
1556+ $ document ['$internalId ' ] = $ internalIds [$ document ->getId ()];
1557+ }
1558+ }
1559+ } catch (PDOException $ e ) {
1560+ throw $ this ->processException ($ e );
1561+ }
1562+
1563+ return $ documents ;
1564+ }
1565+
13331566 /**
13341567 * Increase or decrease an attribute value
13351568 *
0 commit comments