Fix double-counting: let SQL triggers manage ref_count
Previously both Python code AND SQL triggers were incrementing/decrementing ref_count, causing inconsistent values. Now: - New artifacts start with ref_count=0 (triggers increment on tag creation) - Tag INSERT/UPDATE/DELETE triggers handle all ref_count changes - Python code only logs operations, doesn't manipulate ref_count - Delete endpoints rely on cascade + triggers for ref_count management Tested: new uploads, duplicates, tag deletion, package deletion (cascade), project deletion (cascade) - all ref_count values now correct.
This commit is contained in:
@@ -186,17 +186,17 @@ def _create_or_update_tag(
|
|||||||
)
|
)
|
||||||
db.add(history)
|
db.add(history)
|
||||||
|
|
||||||
# Decrement ref_count on old artifact
|
|
||||||
old_ref_count = _decrement_ref_count(db, old_artifact_id)
|
|
||||||
logger.info(
|
|
||||||
f"Tag '{tag_name}' updated: decremented ref_count on artifact "
|
|
||||||
f"{old_artifact_id[:12]}... to {old_ref_count}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update tag to point to new artifact
|
# Update tag to point to new artifact
|
||||||
|
# NOTE: SQL trigger (tags_ref_count_update_trigger) handles ref_count:
|
||||||
|
# - Decrements old artifact's ref_count
|
||||||
|
# - Increments new artifact's ref_count
|
||||||
existing_tag.artifact_id = new_artifact_id
|
existing_tag.artifact_id = new_artifact_id
|
||||||
existing_tag.created_by = user_id
|
existing_tag.created_by = user_id
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Tag '{tag_name}' updated: {old_artifact_id[:12]}... -> {new_artifact_id[:12]}..."
|
||||||
|
)
|
||||||
|
|
||||||
return existing_tag, False, old_artifact_id
|
return existing_tag, False, old_artifact_id
|
||||||
else:
|
else:
|
||||||
# Same artifact, no change needed
|
# Same artifact, no change needed
|
||||||
@@ -488,27 +488,29 @@ def delete_project(
|
|||||||
if not project:
|
if not project:
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
|
|
||||||
# Get all packages in this project
|
# Get counts for logging
|
||||||
packages = db.query(Package).filter(Package.project_id == project.id).all()
|
packages = db.query(Package).filter(Package.project_id == project.id).all()
|
||||||
|
package_count = len(packages)
|
||||||
|
|
||||||
# Get all tags across all packages to decrement their artifact ref_counts
|
|
||||||
artifacts_decremented = {}
|
|
||||||
total_tags = 0
|
total_tags = 0
|
||||||
|
artifact_ids = set()
|
||||||
for package in packages:
|
for package in packages:
|
||||||
tags = db.query(Tag).filter(Tag.package_id == package.id).all()
|
tags = db.query(Tag).filter(Tag.package_id == package.id).all()
|
||||||
total_tags += len(tags)
|
total_tags += len(tags)
|
||||||
|
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag.artifact_id not in artifacts_decremented:
|
artifact_ids.add(tag.artifact_id)
|
||||||
new_ref_count = _decrement_ref_count(db, tag.artifact_id)
|
|
||||||
artifacts_decremented[tag.artifact_id] = new_ref_count
|
|
||||||
logger.info(
|
|
||||||
f"Project '{project_name}' deletion: decremented ref_count on artifact "
|
|
||||||
f"{tag.artifact_id[:12]}... to {new_ref_count}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Audit log
|
logger.info(
|
||||||
|
f"Project '{project_name}' deletion: {package_count} packages, "
|
||||||
|
f"{total_tags} tags affecting {len(artifact_ids)} artifacts"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete the project (cascade will delete packages, tags, etc.)
|
||||||
|
# NOTE: SQL triggers (tags_ref_count_delete_trigger) handle ref_count automatically
|
||||||
|
db.delete(project)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Audit log (after commit)
|
||||||
_log_audit(
|
_log_audit(
|
||||||
db,
|
db,
|
||||||
action="delete_project",
|
action="delete_project",
|
||||||
@@ -516,14 +518,11 @@ def delete_project(
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
source_ip=request.client.host if request.client else None,
|
source_ip=request.client.host if request.client else None,
|
||||||
details={
|
details={
|
||||||
"packages_deleted": len(packages),
|
"packages_deleted": package_count,
|
||||||
"tags_deleted": total_tags,
|
"tags_deleted": total_tags,
|
||||||
"artifacts_affected": list(artifacts_decremented.keys()),
|
"artifacts_affected": list(artifact_ids),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete the project (cascade will delete packages, tags, etc.)
|
|
||||||
db.delete(project)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -868,21 +867,22 @@ def delete_package(
|
|||||||
if not package:
|
if not package:
|
||||||
raise HTTPException(status_code=404, detail="Package not found")
|
raise HTTPException(status_code=404, detail="Package not found")
|
||||||
|
|
||||||
# Get all tags in this package to decrement their artifact ref_counts
|
# Get tags count and affected artifacts for logging
|
||||||
tags = db.query(Tag).filter(Tag.package_id == package.id).all()
|
tags = db.query(Tag).filter(Tag.package_id == package.id).all()
|
||||||
|
artifact_ids = list(set(tag.artifact_id for tag in tags))
|
||||||
|
tag_count = len(tags)
|
||||||
|
|
||||||
# Decrement ref_count for each artifact referenced by tags
|
logger.info(
|
||||||
artifacts_decremented = {}
|
f"Package '{package_name}' deletion: {tag_count} tags affecting "
|
||||||
for tag in tags:
|
f"{len(artifact_ids)} artifacts"
|
||||||
if tag.artifact_id not in artifacts_decremented:
|
)
|
||||||
new_ref_count = _decrement_ref_count(db, tag.artifact_id)
|
|
||||||
artifacts_decremented[tag.artifact_id] = new_ref_count
|
|
||||||
logger.info(
|
|
||||||
f"Package '{package_name}' deletion: decremented ref_count on artifact "
|
|
||||||
f"{tag.artifact_id[:12]}... to {new_ref_count}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Audit log
|
# Delete the package (cascade will delete tags, which triggers ref_count decrements)
|
||||||
|
# NOTE: SQL triggers (tags_ref_count_delete_trigger) handle ref_count automatically
|
||||||
|
db.delete(package)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Audit log (after commit)
|
||||||
_log_audit(
|
_log_audit(
|
||||||
db,
|
db,
|
||||||
action="delete_package",
|
action="delete_package",
|
||||||
@@ -890,13 +890,10 @@ def delete_package(
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
source_ip=request.client.host if request.client else None,
|
source_ip=request.client.host if request.client else None,
|
||||||
details={
|
details={
|
||||||
"tags_deleted": len(tags),
|
"tags_deleted": tag_count,
|
||||||
"artifacts_affected": list(artifacts_decremented.keys()),
|
"artifacts_affected": artifact_ids,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete the package (cascade will delete tags, uploads, consumers)
|
|
||||||
db.delete(package)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -982,8 +979,9 @@ def upload_artifact(
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if artifact:
|
if artifact:
|
||||||
# Artifact exists - increment ref_count (already locked)
|
# Artifact exists - this is a deduplicated upload
|
||||||
artifact.ref_count += 1
|
# NOTE: ref_count is managed by SQL triggers on tag INSERT/DELETE
|
||||||
|
# We don't manually increment here - the tag creation will trigger the increment
|
||||||
deduplicated = True
|
deduplicated = True
|
||||||
saved_bytes = storage_result.size
|
saved_bytes = storage_result.size
|
||||||
# Merge metadata if new metadata was extracted
|
# Merge metadata if new metadata was extracted
|
||||||
@@ -1001,6 +999,9 @@ def upload_artifact(
|
|||||||
# Refresh to get updated ref_count
|
# Refresh to get updated ref_count
|
||||||
db.refresh(artifact)
|
db.refresh(artifact)
|
||||||
else:
|
else:
|
||||||
|
# Create new artifact with ref_count=0
|
||||||
|
# NOTE: ref_count is managed by SQL triggers on tag INSERT/DELETE
|
||||||
|
# When a tag is created for this artifact, the trigger will increment ref_count
|
||||||
artifact = Artifact(
|
artifact = Artifact(
|
||||||
id=storage_result.sha256,
|
id=storage_result.sha256,
|
||||||
size=storage_result.size,
|
size=storage_result.size,
|
||||||
@@ -1012,6 +1013,7 @@ def upload_artifact(
|
|||||||
created_by=user_id,
|
created_by=user_id,
|
||||||
s3_key=storage_result.s3_key,
|
s3_key=storage_result.s3_key,
|
||||||
artifact_metadata=file_metadata or {},
|
artifact_metadata=file_metadata or {},
|
||||||
|
ref_count=0, # Triggers will manage this
|
||||||
)
|
)
|
||||||
db.add(artifact)
|
db.add(artifact)
|
||||||
|
|
||||||
@@ -1910,14 +1912,16 @@ def delete_tag(
|
|||||||
db.add(history)
|
db.add(history)
|
||||||
db.flush() # Flush history before deleting tag (cascade will delete history)
|
db.flush() # Flush history before deleting tag (cascade will delete history)
|
||||||
|
|
||||||
# Decrement ref_count on artifact
|
# NOTE: ref_count decrement is handled by SQL trigger (tags_ref_count_delete_trigger)
|
||||||
new_ref_count = _decrement_ref_count(db, artifact_id)
|
# when the tag is deleted below
|
||||||
logger.info(
|
logger.info(f"Tag '{tag_name}' deleted for artifact {artifact_id[:12]}...")
|
||||||
f"Tag '{tag_name}' deleted: decremented ref_count on artifact "
|
|
||||||
f"{artifact_id[:12]}... to {new_ref_count}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Audit log
|
# Delete the tag (SQL trigger will decrement ref_count)
|
||||||
|
db.delete(tag)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Audit log (after commit so we can query the updated ref_count)
|
||||||
|
artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first()
|
||||||
_log_audit(
|
_log_audit(
|
||||||
db,
|
db,
|
||||||
action="delete_tag",
|
action="delete_tag",
|
||||||
@@ -1926,13 +1930,10 @@ def delete_tag(
|
|||||||
source_ip=request.client.host if request.client else None,
|
source_ip=request.client.host if request.client else None,
|
||||||
details={
|
details={
|
||||||
"artifact_id": artifact_id,
|
"artifact_id": artifact_id,
|
||||||
"new_ref_count": new_ref_count,
|
"ref_count_after": artifact.ref_count if artifact else 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
db.commit() # Commit the audit log
|
||||||
# Delete the tag
|
|
||||||
db.delete(tag)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user