Repository: frappe/frappe

Issue:
bench cannot restore site backup to newly created site
<!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to Frappe
    - For questions and general support, use https://stackoverflow.com/questions/tagged/frappe
    - For documentation issues, refer to https://frappeframework.com/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->

## Description of the issue

Failed to restore backup to newly created site due to sql parsing error.

## Context information (for bug reports)

**Output of `bench version`**
```
erpnext 14.35.0
frappe 14.45.0
mariadb 10.6 CS
```

## Steps to reproduce the issue

1. backup existing site
2. create new site
3. restore backup to new site

### Observed result
The restore process failed when restoring database from backup.

### Expected result
Successfully restore the backup into newly created site

### Stacktrace / full error message

```
frappe@e5af62d4bed7:~/frappe-bench$ bench --site TEST-NEW restore --install-app erpnext --mariadb-root-username root --mariadb-root-password '<redacted>' --admin-password '<redacted>'  sites/TEST-BOM/private/backups/20231010_225922-TEST-BOM-database.sql --with-public-files sites/TEST-BOM/private/backups/20231010_225922-TEST-BOM-files.tar --with-private-files sites/TEST-BOM/private/backups/20231010_225
922-TEST-BOM-private-files.tar                                                                                                                                                                              
Traceback (most recent call last):                                                                                                                                                                          
  File "/usr/local/lib/python3.10/runpy.py", line 196, in _run_module_as_main                                                                                                                               
    return _run_code(code, main_globals, None,                                                                                                                                                              
  File "/usr/local/lib/python3.10/runpy.py", line 86, in _run_code                                                                                                                                          
    exec(code, run_globals)                                                                                                                                                                                 
  File "/home/frappe/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", line 109, in <module>                                                                                                          
    main()                                                                                                                                                                                                  
  File "/home/frappe/frappe-bench/apps/frappe/frappe/utils/bench_helper.py", line 18, in main                                                                                                               
    click.Group(commands=commands)(prog_name="bench")                                                                                                                                                       
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/core.py", line 829, in __call__                                                                                                    
    return self.main(*args, **kwargs)                                                                                                                                                                       
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/core.py", line 782, in main                                                                                                        
    rv = self.invoke(ctx)                                                                                                                                                                                   
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/core.py", line 1259, in invoke                                                                                                     
    return _process_result(sub_ctx.command.invoke(sub_ctx))                                                                                                                                                 
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/core.py", line 1259, in invoke                                                                                                     
    return _process_result(sub_ctx.command.invoke(sub_ctx))                                                                                                                                                 
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/core.py", line 1066, in invoke                                                                                                     
    return ctx.invoke(self.callback, **ctx.params)                                                                                                                                                          
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/core.py", line 610, in invoke                                                                                                      
    return callback(*args, **kwargs)                                                                                                                                                                        
  File "/home/frappe/frappe-bench/env/lib/python3.10/site-packages/click/decorators.py", line 21, in new_func                                                                                               
    return f(get_current_context(), *args, **kwargs)                                                                                                                                                        
  File "/home/frappe/frappe-bench/apps/frappe/frappe/commands/__init__.py", line 29, in _func                                                                                                               
    ret = f(frappe._dict(ctx.obj), *args, **kwargs)                                                                                                                                                         
  File "/home/frappe/frappe-bench/apps/frappe/frappe/commands/site.py", line 202, in restore
    if not force and is_downgrade(decompressed_file_name, verbose=True):
  File "/home/frappe/frappe-bench/apps/frappe/frappe/installer.py", line 789, in is_downgrade
    app_rows = frappe.safe_eval(line)
  File "/home/frappe/frappe-bench/apps/frappe/frappe/__init__.py", line 2296, in safe_eval
    return eval(code, eval_globals, eval_locals)
  File "<string>", line 0
     
SyntaxError: invalid syntax
```

In function [is_downgrade in frappe/installer.py](https://github.com/frappe/frappe/blob/v14.45.0/frappe/installer.py#L782)

```python
def is_downgrade(sql_file_path, verbose=False):
	"""checks if input db backup will get downgraded on current bench"""
        # ...
	head = "INSERT INTO `tabInstalled Application` VALUES"

	with open(sql_file_path) as f:
		for line in f:
			if head in line:
				# 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master')
				line = line.strip().lstrip(head).rstrip(";").strip()
				app_rows = frappe.safe_eval(line)
```

When reading mariadb sql backup file, it seems to expect a single line of SQL insert statement:

```sql
INSERT INTO `tabInstalled Application` VALUES (...), (...);
```

But the actual backup file using multiple row for the insert statement ([default behavior of mariadbdump](https://mariadb.com/docs/server/ref/mdb/cli/mariadb-dump/extended-insert/)). This is the example insert statement of the backup file.

```sql
--
-- Dumping data for table `tabInstalled Application`
--

LOCK TABLES `tabInstalled Application` WRITE;
/*!40000 ALTER TABLE `tabInstalled Application` DISABLE KEYS */;
INSERT INTO `tabInstalled Application` VALUES
('0d6e6f2504','2023-08-20 00:45:30.792694','2023-08-20 00:45:30.792694','Administrator','Administrator',0,2,'erpnext','14.35.0','UNVERSIONED','Installed Applications','installed_applications','Installed Applications'),
('e663d2924e','2023-08-20 00:45:30.790240','2023-08-20 00:45:30.790240','Administrator','Administrator',0,1,'frappe','14.45.0','UNVERSIONED','Installed Applications','installed_applications','Installed Applications');
/*!40000 ALTER TABLE `tabInstalled Application` ENABLE KEYS */;
```

## Additional information

Frappe, erpnext & mariadb all running in containers



Discussion:

Resolving patch:
diff --git a/frappe/installer.py b/frappe/installer.py
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -785,37 +785,20 @@ def is_downgrade(sql_file_path, verbose=False):
 	head = "INSERT INTO `tabInstalled Application` VALUES"
 
 	with open(sql_file_path) as f:
-		for line in f:
-			if head in line:
-				# 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master')
-				line = line.strip().lstrip(head).rstrip(";").strip()
-				app_rows = frappe.safe_eval(line)
-				# check if iterable consists of tuples before trying to transform
-				apps_list = (
-					app_rows
-					if all(isinstance(app_row, (tuple, list, set)) for app_row in app_rows)
-					else (app_rows,)
-				)
-				# 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')]
-				all_apps = [x[-3:] for x in apps_list]
-
-				for app in all_apps:
-					app_name = app[0]
-					app_version = app[1].split(" ", 1)[0]
-
-					if app_name == "frappe":
-						try:
-							current_version = Version(frappe.__version__)
-							backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version)
-						except ValueError:
-							return False
-
-						downgrade = backup_version > current_version
-
-						if verbose and downgrade:
-							print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}")
-
-						return downgrade
+		header = f.readline()
+		# Example first line:
+		# -- Backup generated by Frappe 15.1.0 on branch fix-backup-restore
+
+		if match := re.search(r"Frappe (\d+\.\d+\.\d+)", header):
+			backup_version = Version(match.group(1))
+			current_version = Version(frappe.__version__)
+
+			downgrade = backup_version > current_version
+
+			if verbose and downgrade:
+				print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}")
+
+			return downgrade
 
 
 def is_partial(sql_file_path):


Test patch: