Skip to content

feat: validate datastore write records - #16

Open
luccasmmg wants to merge 1 commit into
mainfrom
feat/datastore-row-validation
Open

feat: validate datastore write records#16
luccasmmg wants to merge 1 commit into
mainfrom
feat/datastore-row-validation

Conversation

@luccasmmg

@luccasmmg luccasmmg commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Validate datastore_create seed records and datastore_upsert records against the stored Frictionless schema.
  • Keep validation in the service layer so all storage backends share the behavior.
  • Return CKAN-compatible validation errors before any backend write.

Verification

  • uv run pytest
  • uv run ruff check datastore tests
  • 420 tests passed.

Summary by CodeRabbit

  • Bug Fixes
    • Records are now validated against their schema before create and upsert operations.
    • Invalid records are rejected before resources are created or updated.
    • Validation errors now include resource- and field-level details to help identify issues.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Datastore write validation

Layer / File(s) Summary
Record validation and service integration
datastore/services/write.py
The write service validates records with Frictionless. Create operations use the supplied schema. Upsert operations load the stored schema. Validation failures become ValidationError instances with resource and field details.
Create and upsert validation coverage
tests/test_write_service.py
Tests verify that invalid records fail before CKAN resource creation or datastore engine writes. Tests also verify field-level validation details.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WriteService
  participant Frictionless
  participant CKAN
  participant DatastoreEngine

  Client->>WriteService: create_datastore(records, schema)
  WriteService->>Frictionless: validate records
  alt records invalid
    Frictionless-->>WriteService: validation report
    WriteService-->>Client: ValidationError
  else records valid
    WriteService->>CKAN: create resource
  end

  Client->>WriteService: upsert_datastore(records)
  WriteService->>CKAN: load stored resource schema
  WriteService->>Frictionless: validate records
  alt records invalid
    Frictionless-->>WriteService: validation report
    WriteService-->>Client: ValidationError
  else records valid
    WriteService->>DatastoreEngine: write records
  end
Loading

Suggested reviewers: sagargg

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: validation of datastore write records.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/datastore-row-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@datastore/services/write.py`:
- Around line 175-177: Update upsert_datastore and BigQueryBackend.upsert so the
schema is read once within a backend-managed atomic operation and reused for
both _validate_records and the subsequent MERGE/INSERT DML. Remove the separate
pre-validation engine.info read, and pass the validated records or validation
callback into the atomic backend flow while preserving existing validation
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28b52a09-0765-4219-91b3-6a32c734820b

📥 Commits

Reviewing files that changed from the base of the PR and between f734403 and d38b32a.

📒 Files selected for processing (2)
  • datastore/services/write.py
  • tests/test_write_service.py

Comment on lines +175 to +177
info = await asyncio.to_thread(engine.info, resource_id)
_validate_records(resource_id=resource_id, schema=info.schema, records=records)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the service read/write boundary.
rg -n -C 8 'engine\.info|engine\.upsert|async def upsert_datastore' datastore/services/write.py

# Inspect engine implementations for transactions or shared synchronization
# that cover schema reads, schema changes, and upserts.
fd -t f -e py . datastore/infrastructure/engines | while IFS= read -r file; do
  rg -n -C 5 'def (info|upsert|create|delete)\b|transaction|Lock|lock|mutex|semaphore' "$file" || true
done

Repository: datopian/datastore

Length of output: 7561


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the full service boundary and engine implementation around the relevant methods.
sed -n '140,195p' datastore/services/write.py
printf '\n--- BigQuery engine relevant methods ---\n'
sed -n '428,490p' datastore/infrastructure/engines/bigquery.py
sed -n '865,925p' datastore/infrastructure/engines/bigquery.py
sed -n '280,340p' datastore/infrastructure/engines/bigquery.py

printf '\n--- Shared lock/mutex candidates ---\n'
rg -n "\bclass .*Lock\b|\b[Aa]syncIO[Aa]ll\w*Lock\b|\bLock\(|\bMutex|threading\..*Lock|asyncio\.(Lock|Semaphore)|Semaphore|condition|Event|contextlib\." datastore/infrastructure datastore/services datastore/core datastore/api datastore/main.py || true

printf '\n--- Other upsert calls to get schema ---\n'
rg -n -C 5 'engine\.info\(|_validate_records\(|\.upsert\(' datastore/services/write.py datastore/api datastore/main.py datastore/tests 2>/dev/null || true

Repository: datopian/datastore

Length of output: 2181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files 'datastore/infrastructure/engines/*.py' 'datastore/services/write.py'

printf '\n--- BigQuery-related files ---\n'
fd -t f -e py 'bigquery|engine|engines' datastore/infrastructure datastore/core datastore/api datastore/services

printf '\n--- Upstream engine info/upsert definitions ---\n'
rg -n -C 8 'def (info|upsert|delete|create)\b|transaction|Lock|lock|mutex|semaphore|BEGIN|COMMIT|rollback' datastore/infrastructure/**/*.py datastore/services/*.py || true

printf '\n--- write.py relevant area ---\n'
sed -n '160,200p' datastore/services/write.py

printf '\n--- upsert calls and info usages ---\n'
rg -n -C 5 'engine\.info\(|\.upsert\(|_validate_records\(' datastore/services datastore/api datastore/main.py datastore/tests 2>/dev/null || true

Repository: datopian/datastore

Length of output: 12810


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- backend.py methods (info/upsert/metadata/schema alters) ---\n'
rg -n -C 10 'def (info|upsert|create|delete|_.*schema|_alter|_apply_schema|_validate|BEGIN|COMMIT|transaction|DELETE_COLUMN|SCHEMA|ALTER TABLE)' datastore/infrastructure/engines/bigquery/backend.py

printf '\n--- backend.py line count ---\n'
wc -l datastore/infrastructure/engines/bigquery/backend.py

printf '\n--- relevant backend slices ---\n'
sed -n '1,160p' datastore/infrastructure/engines/bigquery/backend.py
sed -n '340,470p' datastore/infrastructure/engines/bigquery/backend.py
sed -n '720,870p' datastore/infrastructure/engines/bigquery/backend.py
sed -n '870,950p' datastore/infrastructure/engines/bigquery/backend.py

Repository: datopian/datastore

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- backend.py methods (info/upsert/metadata/schema alters) ---'
rg -n -C 10 'def (info|upsert|create|delete|_.*schema|_alter|_apply_schema|_validate|BEGIN|COMMIT|transaction|DELETE_COLUMN|SCHEMA|ALTER TABLE)' datastore/infrastructure/engines/bigquery/backend.py || true

printf '%s\n' '--- backend.py line count ---'
wc -l datastore/infrastructure/engines/bigquery/backend.py

printf '%s\n' '--- relevant backend slices ---'
sed -n '1,160p' datastore/infrastructure/engines/bigquery/backend.py
sed -n '340,470p' datastore/infrastructure/engines/bigquery/backend.py
sed -n '720,950p' datastore/infrastructure/engines/bigquery/backend.py

printf '%s\n' '--- global lock/mutex/transaction references ---'
rg -n -C 5 'Lock\(|asyncio\.Lock|Semaphore|contextlib\.|threading\.[A-Za-z]*Lock|transaction|BEGIN|COMMIT|ROLLBACK|rollback|_lock|lock' datastore/infrastructure/engines datastore/services datastore/main.py || true

Repository: datopian/datastore

Length of output: 35854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- BigQueryBackend.upsert full implementation ---'
sed -n '460,570p' datastore/infrastructure/engines/bigquery/backend.py

printf '%s\n' '--- BigQueryBackend.create relevant implementation ---'
sed -n '428,460p' datastore/infrastructure/engines/bigquery/backend.py

printf '%s\n' '--- merge DML/schema helpers ---'
sed -n '280,330p' datastore/infrastructure/engines/bigquery/lib.py
sed -n '950,1030p' datastore/infrastructure/engines/bigquery/lib.py

Repository: datopian/datastore

Length of output: 7356


Make the upsert schema read and the BigQuery write atomic.

datastore.services.write.upsert_datastore validates records with engine.info(resource_id).schema, but BigQueryBackend.upsert() reads the schema again before running MERGE/INSERT. A concurrent schema change can make the service validator apply to an old schema while the write succeeds with a different schema, or reject a row that would be valid under the current backend schema. Keep the schema read and the BigQuery DML in one backend-managed atomic block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@datastore/services/write.py` around lines 175 - 177, Update upsert_datastore
and BigQueryBackend.upsert so the schema is read once within a backend-managed
atomic operation and reused for both _validate_records and the subsequent
MERGE/INSERT DML. Remove the separate pre-validation engine.info read, and pass
the validated records or validation callback into the atomic backend flow while
preserving existing validation behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant