batch upload impl
This commit is contained in:
+63
-10
@@ -6,6 +6,46 @@ Package database provides the data access layer for the query orchestration plat
|
||||
|
||||
## Architecture
|
||||
|
||||
### Service Layer Architecture
|
||||
|
||||
The database access follows a clean, layered architecture pattern:
|
||||
|
||||
1. **Repository Layer** (`repository/`)
|
||||
- SQLC-generated code providing type-safe database queries
|
||||
- `Queries` struct contains all database operations
|
||||
- Generated from SQL files in `queries/` directory
|
||||
- Accessed throughout codebase via `ConfigProvider.GetDBQueries()`
|
||||
|
||||
2. **Service Layer** (`internal/*/service.go`)
|
||||
- Each domain has its own service containing business logic
|
||||
- Services use the repository layer for all database access
|
||||
- Examples: `client/service.go`, `document/service.go`, `query/service.go`
|
||||
|
||||
3. **Configuration Layer** (`internal/serviceconfig/database/`)
|
||||
- Manages database connections and connection pooling
|
||||
- Provides transaction support via `ExecuteDBTransaction()`
|
||||
- Handles migration execution on startup
|
||||
|
||||
4. **Database Access Pattern**
|
||||
```
|
||||
API Controllers → Service Layer (business logic)
|
||||
↓
|
||||
ConfigProvider.GetDBQueries()
|
||||
↓
|
||||
Repository Layer (SQLC-generated)
|
||||
↓
|
||||
PostgreSQL Database
|
||||
```
|
||||
|
||||
### Architecture Principles
|
||||
|
||||
- **No Direct SQL in Business Logic**: All SQL queries are defined in `.sql` files
|
||||
- **Type Safety**: SQLC generates Go types from SQL schemas
|
||||
- **Clean Separation**: HTTP handlers → Services → Repository → Database
|
||||
- **Transaction Support**: Coordinated through ConfigProvider interface
|
||||
- **Code Generation**: Repository code is generated, not hand-written
|
||||
- **Single Source of Truth**: Database schema defined in migrations
|
||||
|
||||
### Core Components
|
||||
|
||||
#### Database Schema
|
||||
@@ -16,6 +56,7 @@ The database schema supports document processing and query orchestration with th
|
||||
- **Collectors**: Query collection specifications per client
|
||||
- **Results**: Processed query outputs linked to documents
|
||||
- **Text Extractions**: OCR results from document processing
|
||||
- **Batch Uploads**: Batch document upload tracking with progress monitoring
|
||||
|
||||
#### Migration System
|
||||
- Uses `golang-migrate` for schema version management
|
||||
@@ -53,6 +94,7 @@ The database schema supports document processing and query orchestration with th
|
||||
- **results**: Query execution results per document
|
||||
- **collectors**: Client-specific query collections
|
||||
- **collectorVersions**: Collector version history
|
||||
- **batch_uploads**: Batch document upload tracking with progress and failure metrics
|
||||
|
||||
### Views
|
||||
|
||||
@@ -83,21 +125,20 @@ The database schema supports document processing and query orchestration with th
|
||||
// Create database configuration
|
||||
dbConfig := &serviceconfig.DBConfig{
|
||||
DBHost: "localhost",
|
||||
DBPort: "5432",
|
||||
DBPort: 5432,
|
||||
DBName: "querydb",
|
||||
DBUser: "dbuser",
|
||||
DBPassword: "dbpass",
|
||||
}
|
||||
|
||||
// Get connection pool
|
||||
pool, err := dbConfig.GetDBPool(ctx)
|
||||
// Initialize connection pool
|
||||
err := dbConfig.SetDBPoolConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Create repository queries instance
|
||||
queries := repository.New(pool)
|
||||
// Get repository queries instance
|
||||
queries := dbConfig.GetDBQueries()
|
||||
```
|
||||
|
||||
### Transaction Management
|
||||
@@ -168,6 +209,15 @@ err := migrator.Up()
|
||||
|
||||
## SQLC Integration
|
||||
|
||||
### Configuration
|
||||
SQLC is configured via `sqlc.yml` in the project root with:
|
||||
- PostgreSQL engine with pgx/v5 driver
|
||||
- Queries sourced from `internal/database/queries/`
|
||||
- Schema from `internal/database/migrations/`
|
||||
- Generated code output to `internal/database/repository/`
|
||||
- Custom rules enforcing query performance and safety
|
||||
- UUID type mapping to `github.com/google/uuid`
|
||||
|
||||
### Query Definition Format
|
||||
|
||||
```sql
|
||||
@@ -187,12 +237,15 @@ SET status = $2, updated = CURRENT_TIMESTAMP
|
||||
WHERE id = $1;
|
||||
```
|
||||
|
||||
### Generated Code
|
||||
### Generated Code Features
|
||||
SQLC generates:
|
||||
- Type-safe query functions
|
||||
- Type-safe query functions with parameter structs
|
||||
- Struct definitions matching table schemas
|
||||
- Null handling with sql.NullString, sql.NullTime, etc.
|
||||
- Parameter validation
|
||||
- Null handling with pointers for nullable fields
|
||||
- UUID type support via google/uuid package
|
||||
- SQL comments included in generated code
|
||||
- Enum validation methods
|
||||
- Empty slice initialization for array returns
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Remove storage columns and constraint
|
||||
ALTER TABLE batch_uploads DROP CONSTRAINT IF EXISTS batch_storage_required;
|
||||
ALTER TABLE batch_uploads DROP COLUMN IF EXISTS file_size_bytes;
|
||||
ALTER TABLE batch_uploads DROP COLUMN IF EXISTS archive_key;
|
||||
ALTER TABLE batch_uploads DROP COLUMN IF EXISTS archive_bucket;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Add storage metadata columns to batch_uploads table
|
||||
ALTER TABLE batch_uploads ADD COLUMN archive_bucket text;
|
||||
ALTER TABLE batch_uploads ADD COLUMN archive_key text;
|
||||
ALTER TABLE batch_uploads ADD COLUMN file_size_bytes bigint;
|
||||
|
||||
-- Add constraint to ensure storage metadata is consistent (if any storage field is set, bucket and key must both be set)
|
||||
ALTER TABLE batch_uploads ADD CONSTRAINT batch_storage_consistent
|
||||
CHECK (
|
||||
(archive_bucket IS NULL AND archive_key IS NULL AND file_size_bytes IS NULL) OR
|
||||
(archive_bucket IS NOT NULL AND archive_key IS NOT NULL)
|
||||
);
|
||||
@@ -2,6 +2,18 @@
|
||||
INSERT INTO batch_uploads (client_id, original_filename, total_documents)
|
||||
VALUES ($1, $2, $3) RETURNING id;
|
||||
|
||||
-- name: CreateBatchUploadWithStorage :one
|
||||
INSERT INTO batch_uploads (
|
||||
client_id,
|
||||
original_filename,
|
||||
total_documents,
|
||||
status,
|
||||
archive_bucket,
|
||||
archive_key,
|
||||
file_size_bytes
|
||||
) VALUES ($1, $2, $3, $4::batch_status, $5, $6, $7)
|
||||
RETURNING id, created_at;
|
||||
|
||||
-- name: GetBatchUpload :one
|
||||
SELECT id, client_id, original_filename, total_documents, processed_documents,
|
||||
failed_documents, invalid_type_documents, status, progress_percent,
|
||||
@@ -9,6 +21,14 @@ SELECT id, client_id, original_filename, total_documents, processed_documents,
|
||||
FROM batch_uploads
|
||||
WHERE id = $1 AND client_id = $2;
|
||||
|
||||
-- name: GetBatchUploadWithStorage :one
|
||||
SELECT id, client_id, original_filename, total_documents,
|
||||
processed_documents, failed_documents, invalid_type_documents,
|
||||
status, archive_bucket, archive_key, file_size_bytes,
|
||||
failed_filenames, created_at, completed_at
|
||||
FROM batch_uploads
|
||||
WHERE id = $1 AND client_id = $2;
|
||||
|
||||
-- name: ListBatchUploads :many
|
||||
SELECT id, client_id, original_filename, total_documents, processed_documents,
|
||||
failed_documents, invalid_type_documents, status, progress_percent,
|
||||
|
||||
@@ -96,6 +96,61 @@ func (q *Queries) CreateBatchUpload(ctx context.Context, arg *CreateBatchUploadP
|
||||
return id, err
|
||||
}
|
||||
|
||||
const createBatchUploadWithStorage = `-- name: CreateBatchUploadWithStorage :one
|
||||
INSERT INTO batch_uploads (
|
||||
client_id,
|
||||
original_filename,
|
||||
total_documents,
|
||||
status,
|
||||
archive_bucket,
|
||||
archive_key,
|
||||
file_size_bytes
|
||||
) VALUES ($1, $2, $3, $4::batch_status, $5, $6, $7)
|
||||
RETURNING id, created_at
|
||||
`
|
||||
|
||||
type CreateBatchUploadWithStorageParams struct {
|
||||
ClientID string `db:"client_id"`
|
||||
OriginalFilename string `db:"original_filename"`
|
||||
TotalDocuments int32 `db:"total_documents"`
|
||||
Column4 BatchStatus `db:"column_4"`
|
||||
ArchiveBucket *string `db:"archive_bucket"`
|
||||
ArchiveKey *string `db:"archive_key"`
|
||||
FileSizeBytes *int64 `db:"file_size_bytes"`
|
||||
}
|
||||
|
||||
type CreateBatchUploadWithStorageRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
CreatedAt pgtype.Timestamp `db:"created_at"`
|
||||
}
|
||||
|
||||
// CreateBatchUploadWithStorage
|
||||
//
|
||||
// INSERT INTO batch_uploads (
|
||||
// client_id,
|
||||
// original_filename,
|
||||
// total_documents,
|
||||
// status,
|
||||
// archive_bucket,
|
||||
// archive_key,
|
||||
// file_size_bytes
|
||||
// ) VALUES ($1, $2, $3, $4::batch_status, $5, $6, $7)
|
||||
// RETURNING id, created_at
|
||||
func (q *Queries) CreateBatchUploadWithStorage(ctx context.Context, arg *CreateBatchUploadWithStorageParams) (*CreateBatchUploadWithStorageRow, error) {
|
||||
row := q.db.QueryRow(ctx, createBatchUploadWithStorage,
|
||||
arg.ClientID,
|
||||
arg.OriginalFilename,
|
||||
arg.TotalDocuments,
|
||||
arg.Column4,
|
||||
arg.ArchiveBucket,
|
||||
arg.ArchiveKey,
|
||||
arg.FileSizeBytes,
|
||||
)
|
||||
var i CreateBatchUploadWithStorageRow
|
||||
err := row.Scan(&i.ID, &i.CreatedAt)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getBatchUpload = `-- name: GetBatchUpload :one
|
||||
SELECT id, client_id, original_filename, total_documents, processed_documents,
|
||||
failed_documents, invalid_type_documents, status, progress_percent,
|
||||
@@ -109,6 +164,21 @@ type GetBatchUploadParams struct {
|
||||
ClientID string `db:"client_id"`
|
||||
}
|
||||
|
||||
type GetBatchUploadRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
ClientID string `db:"client_id"`
|
||||
OriginalFilename string `db:"original_filename"`
|
||||
TotalDocuments int32 `db:"total_documents"`
|
||||
ProcessedDocuments int32 `db:"processed_documents"`
|
||||
FailedDocuments int32 `db:"failed_documents"`
|
||||
InvalidTypeDocuments int32 `db:"invalid_type_documents"`
|
||||
Status BatchStatus `db:"status"`
|
||||
ProgressPercent int32 `db:"progress_percent"`
|
||||
FailedFilenames []byte `db:"failed_filenames"`
|
||||
CreatedAt pgtype.Timestamp `db:"created_at"`
|
||||
CompletedAt pgtype.Timestamp `db:"completed_at"`
|
||||
}
|
||||
|
||||
// GetBatchUpload
|
||||
//
|
||||
// SELECT id, client_id, original_filename, total_documents, processed_documents,
|
||||
@@ -116,9 +186,9 @@ type GetBatchUploadParams struct {
|
||||
// failed_filenames, created_at, completed_at
|
||||
// FROM batch_uploads
|
||||
// WHERE id = $1 AND client_id = $2
|
||||
func (q *Queries) GetBatchUpload(ctx context.Context, arg *GetBatchUploadParams) (*BatchUpload, error) {
|
||||
func (q *Queries) GetBatchUpload(ctx context.Context, arg *GetBatchUploadParams) (*GetBatchUploadRow, error) {
|
||||
row := q.db.QueryRow(ctx, getBatchUpload, arg.ID, arg.ClientID)
|
||||
var i BatchUpload
|
||||
var i GetBatchUploadRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ClientID,
|
||||
@@ -136,6 +206,67 @@ func (q *Queries) GetBatchUpload(ctx context.Context, arg *GetBatchUploadParams)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getBatchUploadWithStorage = `-- name: GetBatchUploadWithStorage :one
|
||||
SELECT id, client_id, original_filename, total_documents,
|
||||
processed_documents, failed_documents, invalid_type_documents,
|
||||
status, archive_bucket, archive_key, file_size_bytes,
|
||||
failed_filenames, created_at, completed_at
|
||||
FROM batch_uploads
|
||||
WHERE id = $1 AND client_id = $2
|
||||
`
|
||||
|
||||
type GetBatchUploadWithStorageParams struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
ClientID string `db:"client_id"`
|
||||
}
|
||||
|
||||
type GetBatchUploadWithStorageRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
ClientID string `db:"client_id"`
|
||||
OriginalFilename string `db:"original_filename"`
|
||||
TotalDocuments int32 `db:"total_documents"`
|
||||
ProcessedDocuments int32 `db:"processed_documents"`
|
||||
FailedDocuments int32 `db:"failed_documents"`
|
||||
InvalidTypeDocuments int32 `db:"invalid_type_documents"`
|
||||
Status BatchStatus `db:"status"`
|
||||
ArchiveBucket *string `db:"archive_bucket"`
|
||||
ArchiveKey *string `db:"archive_key"`
|
||||
FileSizeBytes *int64 `db:"file_size_bytes"`
|
||||
FailedFilenames []byte `db:"failed_filenames"`
|
||||
CreatedAt pgtype.Timestamp `db:"created_at"`
|
||||
CompletedAt pgtype.Timestamp `db:"completed_at"`
|
||||
}
|
||||
|
||||
// GetBatchUploadWithStorage
|
||||
//
|
||||
// SELECT id, client_id, original_filename, total_documents,
|
||||
// processed_documents, failed_documents, invalid_type_documents,
|
||||
// status, archive_bucket, archive_key, file_size_bytes,
|
||||
// failed_filenames, created_at, completed_at
|
||||
// FROM batch_uploads
|
||||
// WHERE id = $1 AND client_id = $2
|
||||
func (q *Queries) GetBatchUploadWithStorage(ctx context.Context, arg *GetBatchUploadWithStorageParams) (*GetBatchUploadWithStorageRow, error) {
|
||||
row := q.db.QueryRow(ctx, getBatchUploadWithStorage, arg.ID, arg.ClientID)
|
||||
var i GetBatchUploadWithStorageRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ClientID,
|
||||
&i.OriginalFilename,
|
||||
&i.TotalDocuments,
|
||||
&i.ProcessedDocuments,
|
||||
&i.FailedDocuments,
|
||||
&i.InvalidTypeDocuments,
|
||||
&i.Status,
|
||||
&i.ArchiveBucket,
|
||||
&i.ArchiveKey,
|
||||
&i.FileSizeBytes,
|
||||
&i.FailedFilenames,
|
||||
&i.CreatedAt,
|
||||
&i.CompletedAt,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getDocumentsByBatchId = `-- name: GetDocumentsByBatchId :many
|
||||
SELECT id, clientId, hash
|
||||
FROM documents
|
||||
|
||||
@@ -245,6 +245,9 @@ type BatchUpload struct {
|
||||
FailedFilenames []byte `db:"failed_filenames"`
|
||||
CreatedAt pgtype.Timestamp `db:"created_at"`
|
||||
CompletedAt pgtype.Timestamp `db:"completed_at"`
|
||||
ArchiveBucket *string `db:"archive_bucket"`
|
||||
ArchiveKey *string `db:"archive_key"`
|
||||
FileSizeBytes *int64 `db:"file_size_bytes"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
|
||||
Reference in New Issue
Block a user