django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
  3. django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
  4. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/forms.py +8 -10
  12. treenode/managers/__init__.py +21 -0
  13. treenode/managers/adjacency.py +203 -0
  14. treenode/managers/closure.py +278 -0
  15. treenode/models/__init__.py +2 -1
  16. treenode/models/adjacency.py +343 -0
  17. treenode/models/classproperty.py +3 -0
  18. treenode/models/closure.py +23 -24
  19. treenode/models/factory.py +12 -2
  20. treenode/models/mixins/__init__.py +23 -0
  21. treenode/models/mixins/ancestors.py +65 -0
  22. treenode/models/mixins/children.py +81 -0
  23. treenode/models/mixins/descendants.py +66 -0
  24. treenode/models/mixins/family.py +63 -0
  25. treenode/models/mixins/logical.py +68 -0
  26. treenode/models/mixins/node.py +210 -0
  27. treenode/models/mixins/properties.py +156 -0
  28. treenode/models/mixins/roots.py +96 -0
  29. treenode/models/mixins/siblings.py +99 -0
  30. treenode/models/mixins/tree.py +344 -0
  31. treenode/signals.py +26 -0
  32. treenode/static/treenode/css/tree_widget.css +201 -31
  33. treenode/static/treenode/css/treenode_admin.css +48 -41
  34. treenode/static/treenode/js/tree_widget.js +269 -131
  35. treenode/static/treenode/js/treenode_admin.js +131 -171
  36. treenode/templates/admin/tree_node_changelist.html +6 -0
  37. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  38. treenode/tests/tests.py +488 -0
  39. treenode/urls.py +10 -6
  40. treenode/utils/__init__.py +2 -0
  41. treenode/utils/aid.py +46 -0
  42. treenode/utils/base16.py +38 -0
  43. treenode/utils/base36.py +3 -1
  44. treenode/utils/db.py +116 -0
  45. treenode/utils/exporter.py +2 -0
  46. treenode/utils/importer.py +0 -1
  47. treenode/utils/radix.py +61 -0
  48. treenode/version.py +2 -2
  49. treenode/views.py +118 -43
  50. treenode/widgets.py +91 -43
  51. django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
  52. django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
  53. treenode/admin.py +0 -439
  54. treenode/docs/Documentation +0 -636
  55. treenode/managers.py +0 -419
  56. treenode/models/proxy.py +0 -669
  57. {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,302 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Views Mixin for TreeNodeAdminModel
4
+
5
+ Version: 2.1.0
6
+ Author: Timur Kady
7
+ Email: kaduevtr@gmail.com
8
+ """
9
+
10
+ import os
11
+ from datetime import datetime
12
+ from django.contrib import admin
13
+ from django.contrib import messages
14
+ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
15
+ from django.db import models
16
+ from django.http import JsonResponse
17
+ from django.shortcuts import render, redirect
18
+ from django.shortcuts import resolve_url
19
+ from django.template.loader import render_to_string
20
+ from django.urls import path
21
+ from django.utils.encoding import force_str
22
+ from django.utils.safestring import mark_safe
23
+
24
+ import logging
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class AdminMixin(admin.ModelAdmin):
30
+ """Admin Mixin with views."""
31
+
32
+ def change_list_view(self, request):
33
+ """
34
+ View for lazy loading of child nodes.
35
+
36
+ Clicking the expand button sends an AJAX request to this view, which
37
+ returns JSON with the data of the selected node's immediate children.
38
+ All returned nodes are collapsed by default.
39
+ """
40
+ parent_id = request.GET.get("tn_parent_id")
41
+ if not parent_id:
42
+ # If there is no AJAX parameter, then it's normal work: show roots
43
+ return super().change_list_view(request)
44
+
45
+ parent = self.model.objects.filter(pk=parent_id).first()
46
+ if not parent:
47
+ # If there is no parent, return an empty response
48
+ return JsonResponse({'html': ''})
49
+
50
+ children = parent.get_children_queryset()
51
+
52
+ # We take list_display to understand which "columns" are needed
53
+ list_display = self.get_list_display(request)
54
+
55
+ # We collect future "rows"; each as a list of cells
56
+ rows = []
57
+ td_classes = []
58
+ for obj in children:
59
+ row_data = []
60
+ checkbox = mark_safe(
61
+ f'<input type="checkbox" name="{ACTION_CHECKBOX_NAME}"'
62
+ f' value="{obj.pk}" class="action-select" />'
63
+ )
64
+ row_data.append(checkbox)
65
+ td_classes.append("action-checkbox")
66
+ for field in list_display:
67
+ if callable(field):
68
+ # If it is a method (drag, toggle, etc.), call
69
+ row_data.append(field(obj))
70
+ field_name = field.__name__
71
+ else:
72
+ # If it is a string, we try to get the attribute from
73
+ # the object
74
+ value = getattr(obj, field, '')
75
+ if callable(value):
76
+ # If suddenly this is also a method
77
+ # (for example, property), then call it
78
+ value = value()
79
+ row_data.append(value)
80
+ field_name = field
81
+ td_classes.append(f"field-{field_name}")
82
+
83
+ row_info = {
84
+ "node_id": obj.pk,
85
+ "parent_id": parent_id,
86
+ "cells": zip(row_data, td_classes),
87
+ }
88
+ rows.append(row_info)
89
+
90
+ # Pass rows to the template
91
+ html = render_to_string(
92
+ 'admin/treenode_ajax_rows.html',
93
+ {"rows": rows},
94
+ request=request
95
+ )
96
+
97
+ return JsonResponse({'html': html})
98
+
99
+ def search_view(self, request):
100
+ """View for finding nodes using get_list_display."""
101
+ # Get a search query
102
+ q = request.GET.get("q", "")
103
+
104
+ # Perform filtering with annotation to calculate the level
105
+ queryset = self.model.objects\
106
+ .annotate(cl_depth=models.Max("parents_set__depth"))\
107
+ .filter(name__icontains=q)
108
+ queryset_list = list(queryset)[:20]
109
+ sorted_list = self.model._sort_node_list(queryset_list)
110
+
111
+ # Get the set of columns as it is formed in change_list.
112
+ # The first two columns (drag and toggle) are not needed for searching,
113
+ # and the main display column is at index 2.
114
+ list_display = self.get_list_display(request)
115
+ display_func = list_display[2]
116
+
117
+ results = []
118
+ for node in sorted_list:
119
+ # Get the HTML display of the node via the function generated by
120
+ # get_list_display
121
+ display_html = display_func(node)
122
+ results.append({
123
+ "id": node.pk,
124
+ "text": display_html,
125
+ "level": node.cl_depth,
126
+ "is_leaf": node.is_leaf(),
127
+ })
128
+
129
+ return JsonResponse({"results": results})
130
+
131
+ def import_view(self, request):
132
+ """
133
+ Import View.
134
+
135
+ File upload processing, auto-detection of format, validation and data
136
+ import.
137
+ """
138
+ if not self.import_export:
139
+ self.message_user(
140
+ request,
141
+ "Import functionality is disabled because required \
142
+ packages are not installed."
143
+ )
144
+ return redirect("..")
145
+
146
+ if request.method == 'POST':
147
+ if 'file' not in request.FILES:
148
+ return render(
149
+ request,
150
+ "admin/tree_node_import.html",
151
+ {"errors": ["No file uploaded."]}
152
+ )
153
+
154
+ file = request.FILES['file']
155
+ ext = os.path.splitext(file.name)[-1].lower().strip(".")
156
+
157
+ allowed_formats = {"csv", "json", "xlsx", "yaml", "tsv"}
158
+ if ext not in allowed_formats:
159
+ return render(
160
+ request,
161
+ "admin/tree_node_import.html",
162
+ {"errors": [f"Unsupported file format: {ext}"]}
163
+ )
164
+
165
+ # Import data from file
166
+ importer = self.TreeNodeImporter(self.model, file, ext)
167
+ raw_data = importer.import_data()
168
+ clean_result = importer.finalize(raw_data)
169
+
170
+ errors = clean_result.get("errors", [])
171
+ created_count = len(clean_result.get("create", []))
172
+ updated_count = len(clean_result.get("update", []))
173
+
174
+ if errors:
175
+ return render(
176
+ request,
177
+ "admin/tree_node_import_report.html",
178
+ {
179
+ "errors": errors,
180
+ "created_count": created_count,
181
+ "updated_count": updated_count,
182
+ }
183
+ )
184
+
185
+ # If there are no errors, redirect to the list of objects with
186
+ # a message
187
+ messages.success(
188
+ request,
189
+ f"Successfully imported {created_count} records. "
190
+ f"Successfully updated {updated_count} records."
191
+ )
192
+
193
+ app_label = self.model._meta.app_label
194
+ model_name = self.model._meta.model_name
195
+ admin_changelist_url = f"admin:{app_label}_{model_name}_changelist"
196
+ path = resolve_url(admin_changelist_url) + "?import_done=1"
197
+ return redirect(path)
198
+
199
+ # If the request is not POST, simply display the import form
200
+ return render(request, "admin/tree_node_import.html")
201
+
202
+ def export_view(self, request):
203
+ """
204
+ Export view.
205
+
206
+ - If the GET parameters include download, we send the file directly.
207
+ - If the format parameter is missing, we render the format selection
208
+ page.
209
+ - If the format is specified, we perform a test export to catch errors.
210
+
211
+ If there are no errors, we render the success page with a message, a
212
+ link for manual download,
213
+ and a button to go to the model page.
214
+ """
215
+ if not self.import_export:
216
+ self.message_user(
217
+ request,
218
+ "Export functionality is disabled because required \
219
+ packages are not installed."
220
+ )
221
+ return redirect("..")
222
+
223
+ # If the download parameter is present, we give the file directly
224
+ if 'download' in request.GET:
225
+ # Get file format
226
+ export_format = request.GET.get('format', 'csv')
227
+ # Filename
228
+ now = force_str(datetime.now().strftime("%Y-%m-%d %H-%M"))
229
+ filename = self.model._meta.label + " " + now
230
+ # Init
231
+ exporter = self.TreeNodeExporter(
232
+ self.get_queryset(request),
233
+ filename=filename
234
+ )
235
+ # Export working
236
+ response = exporter.export(export_format)
237
+ logger.debug("DEBUG: File response generated.")
238
+ return response
239
+
240
+ # If the format parameter is not passed, we show the format
241
+ # selection page
242
+ if 'format' not in request.GET:
243
+ return render(request, "admin/tree_node_export.html")
244
+
245
+ # If the format is specified, we try to perform a test export
246
+ # (without returning the file)
247
+ export_format = request.GET['format']
248
+ exporter = self.TreeNodeExporter(
249
+ self.model.objects.all(),
250
+ filename=self.model._meta.model_name
251
+ )
252
+ try:
253
+ # Test call to check for export errors (result not used)
254
+ exporter.export(export_format)
255
+ except Exception as e:
256
+ logger.error("Error during test export: %s", e)
257
+ errors = [str(e)]
258
+ return render(
259
+ request,
260
+ "admin/tree_node_export.html",
261
+ {"errors": errors}
262
+ )
263
+
264
+ # Form the correct download URL. If the URL already contains
265
+ # parameters, add them via &download=1, otherwise via ?download=1
266
+ current_url = request.build_absolute_uri()
267
+ if "?" in current_url:
268
+ download_url = current_url + "&download=1"
269
+ else:
270
+ download_url = current_url + "?download=1"
271
+
272
+ context = {
273
+ "download_url": download_url,
274
+ "message": "Your file is ready for export. \
275
+ The download should start automatically.",
276
+ "manual_download_label": "If the download does not start, \
277
+ click this link.",
278
+ # Can be replaced with the desired URL to return to the model
279
+ "redirect_url": "../",
280
+ "button_text": "Return to model"
281
+ }
282
+ return render(request, "admin/export_success.html", context)
283
+
284
+ def get_urls(self):
285
+ """
286
+ Extend admin URLs with custom import/export routes.
287
+
288
+ Register these URLs only if all the required packages are installed.
289
+ """
290
+ urls = super().get_urls()
291
+ if self.import_export:
292
+ custom_urls = [
293
+ path('change_list/', self.change_list_view, name='change_list'),
294
+ path('search/', self.search_view, name='search'),
295
+ path('import/', self.import_view, name='tree_node_import'),
296
+ path('export/', self.export_view, name='tree_node_export'),
297
+ ]
298
+ else:
299
+ custom_urls = []
300
+ return custom_urls + urls
301
+
302
+ # The End
treenode/apps.py CHANGED
@@ -5,13 +5,17 @@ TreeNode Application Configuration
5
5
  This module defines the application configuration for the TreeNode app.
6
6
  It sets the default auto field and specifies the app's name.
7
7
 
8
- Version: 2.0.0
8
+ Version: 2.1.0
9
9
  Author: Timur Kady
10
10
  Email: timurkady@yandex.com
11
11
  """
12
12
 
13
13
 
14
+ import logging
14
15
  from django.apps import AppConfig
16
+ from django.db.models.signals import post_migrate
17
+
18
+ logger = logging.getLogger(__name__)
15
19
 
16
20
 
17
21
  class TreeNodeConfig(AppConfig):
@@ -20,4 +24,11 @@ class TreeNodeConfig(AppConfig):
20
24
  default_auto_field = "django.db.models.BigAutoField"
21
25
  name = "treenode"
22
26
 
27
+ def ready(self):
28
+ """
29
+ Attach a post_migrate handler.
23
30
 
31
+ This allows you to perform operations after the migration is complete.
32
+ """
33
+ from .utils.db import post_migrate_update
34
+ post_migrate.connect(post_migrate_update, sender=self)
treenode/cache.py CHANGED
@@ -212,8 +212,8 @@ def cached_method(func):
212
212
  label,
213
213
  func.__name__,
214
214
  unique_id,
215
- args,
216
- kwargs
215
+ *args,
216
+ **kwargs
217
217
  )
218
218
 
219
219
  # Retrieving from cache
treenode/forms.py CHANGED
@@ -10,13 +10,12 @@ Functions:
10
10
  - __init__: Initializes the form and filters out invalid parent choices.
11
11
  - factory: Dynamically creates a form class for a given TreeNode model.
12
12
 
13
- Version: 2.0.11
13
+ Version: 2.1.0
14
14
  Author: Timur Kady
15
15
  Email: timurkady@yandex.com
16
16
  """
17
17
 
18
18
  from django import forms
19
- import numpy as np
20
19
  from django.forms.models import ModelChoiceField, ModelChoiceIterator
21
20
  from django.utils.translation import gettext_lazy as _
22
21
 
@@ -29,13 +28,12 @@ class SortedModelChoiceIterator(ModelChoiceIterator):
29
28
  def __iter__(self):
30
29
  """Return sorted choices based on tn_order."""
31
30
  qs_list = list(self.queryset.all())
32
- # Sort objects by their tn_order using NumPy.
33
- tn_orders = np.array([obj.tn_order for obj in qs_list])
34
- sorted_indices = np.argsort(tn_orders)
35
- # Iterate over sorted indices and yield (value, label) pairs.
36
- for idx in sorted_indices:
37
- # Cast the index to int if it is numpy.int64.
38
- obj = qs_list[int(idx)]
31
+
32
+ # Sort objects
33
+ sorted_objects = self.queryset.model._sort_node_list(qs_list)
34
+
35
+ # Iterate yield (value, label) pairs.
36
+ for obj in sorted_objects:
39
37
  yield (
40
38
  self.field.prepare_value(obj),
41
39
  self.field.label_from_instance(obj)
@@ -100,7 +98,7 @@ class TreeNodeForm(forms.ModelForm):
100
98
  )
101
99
  self.fields["tn_parent"].widget.model = queryset.model
102
100
 
103
- # Если есть текущее значение, устанавливаем его
101
+ # If there is a current value, set it
104
102
  if self.instance and self.instance.pk and self.instance.tn_parent:
105
103
  self.fields["tn_parent"].initial = self.instance.tn_parent
106
104
 
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Managers and QuerySets
4
+
5
+ This module defines custom managers and query sets for the TreeNode model.
6
+ It includes optimized bulk operations for handling hierarchical data
7
+ using the Closure Table approach.
8
+
9
+ Features:
10
+ - `ClosureModelManager` for managing closure records.
11
+ - `TreeNodeModelManager` for adjacency model operations.
12
+
13
+ Version: 2.1.0
14
+ Author: Timur Kady
15
+ Email: timurkady@yandex.com
16
+ """
17
+
18
+ from .closure import ClosureModelManager
19
+ from .adjacency import TreeNodeModelManager
20
+
21
+ __all__ = ["TreeNodeModelManager", "ClosureModelManager"]
@@ -0,0 +1,203 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Adjacency List Manager and QuerySet
4
+
5
+ This module defines custom managers and query sets for the Adjacency List.
6
+ It includes operations for synchronizing with the model implementing
7
+ the Closure Table.
8
+
9
+ Version: 2.1.0
10
+ Author: Timur Kady
11
+ Email: timurkady@yandex.com
12
+ """
13
+
14
+ from collections import deque, defaultdict
15
+ from django.db import models, transaction
16
+ from django.db import connection
17
+
18
+
19
+ class TreeNodeQuerySet(models.QuerySet):
20
+ """TreeNodeModel QuerySet."""
21
+
22
+ def __init__(self, model=None, query=None, using=None, hints=None):
23
+ # First we call the parent class constructor
24
+ super().__init__(model, query, using, hints)
25
+
26
+ def create(self, **kwargs):
27
+ """Ensure that the save logic is executed when using create."""
28
+ obj = self.model(**kwargs)
29
+ obj.save()
30
+ return obj
31
+
32
+ def update(self, **kwargs):
33
+ """Update node with synchronization of tn_parent change."""
34
+ tn_parent_changed = 'tn_parent' in kwargs
35
+ # Save pks of updated objects
36
+ pks = list(self.values_list('pk', flat=True))
37
+ # Clone the query and clear the ordering to avoid an aggregation error
38
+ qs = self._clone()
39
+ qs.query.clear_ordering()
40
+ result = super(TreeNodeQuerySet, qs).update(**kwargs)
41
+ if tn_parent_changed and pks:
42
+ objs = list(self.model.objects.filter(pk__in=pks))
43
+ self.model.closure_model.objects.bulk_update(objs, ['tn_parent'])
44
+ return result
45
+
46
+ def get_or_create(self, defaults=None, **kwargs):
47
+ """Ensure that the save logic is executed when using get_or_create."""
48
+ defaults = defaults or {}
49
+ created = False
50
+ obj = self.filter(**kwargs).first()
51
+ if obj is None:
52
+ params = {k: v for k, v in kwargs.items() if "__" not in k}
53
+ params.update(
54
+ {k: v() if callable(v) else v for k, v in defaults.items()}
55
+ )
56
+ obj = self.create(**params)
57
+ created = True
58
+ return obj, created
59
+
60
+ def update_or_create(self, defaults=None, create_defaults=None, **kwargs):
61
+ """Update or create."""
62
+ defaults = defaults or {}
63
+ create_defaults = create_defaults or {}
64
+
65
+ with transaction.atomic():
66
+ obj = self.filter(**kwargs).first()
67
+ params = {k: v for k, v in kwargs.items() if "__" not in k}
68
+ if obj is None:
69
+ params.update({k: v() if callable(v) else v for k,
70
+ v in create_defaults.items()})
71
+ obj = self.create(**params)
72
+ created = True
73
+ else:
74
+ params.update(
75
+ {k: v() if callable(v) else v for k, v in defaults.items()})
76
+ for field, value in params.items():
77
+ setattr(obj, field, value)
78
+ obj.save(update_fields=params.keys())
79
+ created = False
80
+ return obj, created
81
+
82
+ def bulk_create(self, objs, batch_size=1000, *args, **kwargs):
83
+ """
84
+ Bulk create.
85
+
86
+ Method of bulk creation objects with updating and processing of
87
+ the Closuse Model.
88
+ """
89
+ # 1. Bulk Insertion of Nodes in Adjacency Models
90
+ objs = super().bulk_create(objs, batch_size, *args, **kwargs)
91
+ # 2. Synchronization of the Closing Model
92
+ self.model.closure_model.objects.bulk_create(objs)
93
+ # 3. Clear cache and return result
94
+ self.model.clear_cache()
95
+ return objs
96
+
97
+ def bulk_update(self, objs, fields, batch_size=1000):
98
+ """Bulk update with synchronization of tn_parent change."""
99
+ # Clone the query and clear the ordering to avoid an aggregation error
100
+ qs = self._clone()
101
+ qs.query.clear_ordering()
102
+ # Perform an Adjacency Model Update
103
+ result = super(TreeNodeQuerySet, qs).bulk_update(
104
+ objs, fields, batch_size
105
+ )
106
+ # Synchronize data in the Closing Model
107
+ if 'tn_parent' in fields:
108
+ self.model.closure_model.objects.bulk_update(
109
+ objs, ['tn_parent'], batch_size
110
+ )
111
+ return result
112
+
113
+
114
+ class TreeNodeModelManager(models.Manager):
115
+ """TreeNodeModel Manager."""
116
+
117
+ def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
118
+ """
119
+ Bulk Create.
120
+
121
+ Override bulk_create for the adjacency model.
122
+ Here we first clear the cache, then delegate the creation via our
123
+ custom QuerySet.
124
+ """
125
+ self.model.clear_cache()
126
+ result = self.get_queryset().bulk_create(
127
+ objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
128
+ )
129
+ transaction.on_commit(lambda: self._update_auto_increment())
130
+ return result
131
+
132
+ def bulk_update(self, objs, fields=None, batch_size=1000):
133
+ """Bulk Update."""
134
+ self.model.clear_cache()
135
+ result = self.get_queryset().bulk_update(objs, fields, batch_size)
136
+ return result
137
+
138
+ def get_queryset(self):
139
+ """Return a sorted QuerySet."""
140
+ queryset = TreeNodeQuerySet(self.model, using=self._db)\
141
+ .annotate(_depth_db=models.Max("parents_set__depth"))\
142
+ .order_by("_depth_db", "tn_parent", "tn_priority")
143
+ return queryset
144
+
145
+ # Service methods -------------------
146
+
147
+ def _bulk_update_tn_closure(self, objs, fields=None, batch_size=1000):
148
+ """Update tn_closure in bulk."""
149
+ self.model.clear_cache()
150
+ super().bulk_update(objs, fields, batch_size)
151
+
152
+ def _get_auto_increment_sequence(self):
153
+ """Get auto increment sequence."""
154
+ table_name = self.model._meta.db_table
155
+ pk_column = self.model._meta.pk.column
156
+ with connection.cursor() as cursor:
157
+ query = "SELECT pg_get_serial_sequence(%s, %s)"
158
+ cursor.execute(query, [table_name, pk_column])
159
+ result = cursor.fetchone()
160
+ return result[0] if result else None
161
+
162
+ def _update_auto_increment(self):
163
+ """Update auto increment."""
164
+ table_name = self.model._meta.db_table
165
+ with connection.cursor() as cursor:
166
+ db_engine = connection.vendor
167
+
168
+ if db_engine == "postgresql":
169
+ sequence_name = self._get_auto_increment_sequence()
170
+ # Get the max id from the table
171
+ cursor.execute(
172
+ f"SELECT COALESCE(MAX(id), 0) FROM {table_name};"
173
+ )
174
+ max_id = cursor.fetchone()[0]
175
+ next_id = max_id + 1
176
+ # Directly specify the next value of the sequence
177
+ cursor.execute(
178
+ f"ALTER SEQUENCE {sequence_name} RESTART WITH {next_id};"
179
+ )
180
+ elif db_engine == "mysql":
181
+ cursor.execute(f"SELECT MAX(id) FROM {table_name};")
182
+ max_id = cursor.fetchone()[0] or 0
183
+ next_id = max_id + 1
184
+ cursor.execute(
185
+ f"ALTER TABLE {table_name} AUTO_INCREMENT = {next_id};"
186
+ )
187
+ elif db_engine == "sqlite":
188
+ cursor.execute(
189
+ f"UPDATE sqlite_sequence SET seq = (SELECT MAX(id) \
190
+ FROM {table_name}) WHERE name='{table_name}';"
191
+ )
192
+ elif db_engine == "mssql":
193
+ cursor.execute(f"SELECT MAX(id) FROM {table_name};")
194
+ max_id = cursor.fetchone()[0] or 0
195
+ cursor.execute(
196
+ f"DBCC CHECKIDENT ('{table_name}', RESEED, {max_id});"
197
+ )
198
+ else:
199
+ raise NotImplementedError(
200
+ f"Autoincrement for {db_engine} is not supported."
201
+ )
202
+
203
+ # The End