Skip to content

feat: Integrate sqlc for type-safe queries#15

Open
kusold wants to merge 3 commits intomainfrom
feature/sqlc-integration
Open

feat: Integrate sqlc for type-safe queries#15
kusold wants to merge 3 commits intomainfrom
feature/sqlc-integration

Conversation

@kusold
Copy link
Owner

@kusold kusold commented Mar 21, 2026

Summary

Replace raw SQL with sqlc-generated code in the PostgresIdentityStore, providing compile-time SQL verification and type safety.

Changes

New Files

  • sqlc.yaml - sqlc configuration file
  • queries/auth.sql - SQL query definitions (sqlc format)
  • schema/auth.sql - Clean schema for sqlc (without goose markers)
  • internal/db/ - sqlc-generated Go code (DO NOT EDIT manually)

Modified Files

  • auth/postgres_store.go - Rewritten to use sqlc-generated queries
  • app/app.go - Handle error from NewPostgresIdentityStore
  • flake.nix - Add sqlc to dev shell

Deleted Files

  • auth/postgres_store_legacy.go - Old implementation removed

Benefits

Compile-Time Safety

  • SQL syntax errors caught at compile time, not runtime
  • No more SQL injection risks from string formatting
  • IDE autocomplete and type hints for all query parameters

Type Safety

  • Query parameters typed (UUID, string, bool, etc.)
  • Result columns automatically mapped to Go structs
  • No manual Scan() required for most queries

Maintainability

  • SQL queries organized in queries/ directory
  • Clear separation between SQL and Go code
  • Easy to view and edit all queries in one place

Developer Experience

  • Better IDE support with code generation
  • Consistent error handling throughout
  • Clear, documented query signatures

Migration Path

  1. sqlc automatically generates type-safe Go code from SQL
  2. PostgresIdentityStore uses generated queries via db.Queries
  3. Schema qualification handled automatically by sqlc
  4. All existing APIs remain unchanged

Testing

  • All existing tests pass ✓
  • No breaking changes to public APIs
  • sqlc-generated code validated with tests

Implementation Details

Schema Qualification

The schema name is passed to sqlc during code generation.
When the PostgresIdentityStore connects to the database, it sets
the search_path to include the configured schema, ensuring
queries use the correct tables.

Security

validateSchemaName() function ensures schema names are safe:

  • Only alphanumeric characters and underscores
  • Max 63 characters
  • Cannot start with a digit
  • Validates before query execution

Generated Code

The generated code includes:

  • Type-safe query functions with parameter structs
  • Generated models for database tables
  • Interface-based query execution for testability
  • No manual Scan() calls needed

Future Enhancements

This integration enables:

  1. Adding sqlc to other packages (tenantctx, observability)
  2. Implementing sqlc-based admin tooling
  3. Easier addition of new database operations
  4. Migration to sqlc for all SQL in the codebase

[Optional] Checklist:

  • All tests pass
  • Code follows project style
  • Documentation updated
  • No breaking changes

Replace raw SQL with sqlc-generated code for the PostgresIdentityStore,
providing compile-time SQL verification and type safety.

Changes:
- Add sqlc.yaml configuration and generated code (internal/db/)
- Add queries/auth.sql with all query definitions
- Add schema/auth.sql for sqlc (clean schema without goose markers)
- Rewrite auth/postgres_store.go to use sqlc-generated queries
- Update app/app.go to handle error from NewPostgresIdentityStore
- Add sqlc to dev shell in flake.nix

Benefits:
- Compile-time SQL syntax validation
- Type-safe query parameters and results
- Query organization in queries/ directory
- Better IDE support and autocomplete
- No SQL string formatting in production code

The schema_aware.go file provides schema qualification support
and security validation for schema names.
Copy link
Owner Author

@kusold kusold left a comment

Choose a reason for hiding this comment

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

🎨 Code Quality Review

Summary

Solid integration of sqlc for type-safe queries. The refactor eliminates manual SQL string construction, improving safety and maintainability. A few issues need attention around error handling patterns.

Findings

✅ Good:

  • Type-safe queries via sqlc eliminates string interpolation SQL injection risks
  • Clean separation of SQL into queries/auth.sql and schema into schema/auth.sql
  • Generated Querier interface (internal/db/querier.go) enables easy mocking/testing
  • Proper error wrapping with context (fmt.Errorf("failed to ...: %w", err))
  • Schema name validation added to prevent injection (validateSchemaName)
  • Added sqlc to dev dependencies in flake.nix

⚠️ Suggestions:

  • auth/postgres_store.go:57-58 and 78-79 - Error comparison uses fragile string matching:

    if err.Error() != "no rows in result set" {

    Consider using a custom error type or checking if sqlc exposes a sentinel error. String comparison breaks if error messages change.

  • Duplicate validation logic - validateSchemaName exists in both:

    • auth/postgres_store.go:209-219
    • internal/db/schema_aware.go:40-53

    Implementations differ slightly (postgres_store version checks if starts with digit, schema_aware doesn't). Consolidate into one location.

  • internal/db/schema_aware.go:32-36 - WithSchema is a no-op. Either implement or remove if schema handling is fully via pool config.

  • internal/db/schema_aware.go:55-57 - QuoteIdentifier is defined but unused. Remove dead code.

  • auth/postgres_store.go:73-80 - Manual pgtype construction is verbose. Consider a helper for common conversions like pgtype.UUID.

🔴 Issues:

  • Missing test coverage - The refactored auth store lacks tests. The Querier interface makes this straightforward - please add tests for the identity store using a mock or test database.

Overall direction is great. The string-based error check is the main concern to address before merge.

Copy link
Owner Author

@kusold kusold left a comment

Choose a reason for hiding this comment

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

🔒 Security Review

Summary

This PR significantly improves security posture by replacing string-interpolated SQL queries with sqlc-generated parameterized queries. The changes eliminate the primary SQL injection vectors from the legacy code. A few minor issues were identified for cleanup.

Findings

🔴 Critical:

  • None identified

⚠️ Warnings:

  • Fragile error handling: String comparison for "no rows" errors (err.Error() != "no rows in result set") is brittle and could break across pgx versions. Consider using pgx.ErrNoRows or wrapping with sentinel errors.
  • Dead code with inconsistent validation: internal/db/schema_aware.go contains a ValidateSchemaName function that allows schema names starting with digits, while auth/postgres_store.go has a stricter version that doesn't. The SchemaAwareQueries type appears unused—consider removing this file to avoid confusion.

✅ Good:

  • SQL injection eliminated: All queries now use parameterized statements via sqlc (, , ... placeholders)
  • Schema name validation: validateSchemaName() enforces alphanumeric + underscore, max 63 chars, can't start with digit
  • Role constraint: Database CHECK constraint limits roles to owner, admin, member
  • Proper error propagation: Constructor now returns errors for invalid schema names
  • Type safety: UUID types enforced at compile time, preventing type confusion attacks
  • No secrets in code: Database credentials remain externalized in configuration

Copy link
Owner Author

@kusold kusold left a comment

Choose a reason for hiding this comment

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

🧪 Testing Review

Summary

This PR introduces sqlc integration with significant refactoring of PostgresIdentityStore, but no new tests were added. The PR description claims "all existing tests pass" but the code paths being modified (PostgresIdentityStore, validateSchemaName) don't appear to have existing unit test coverage.

Findings

🔴 Missing Tests:

  • validateSchemaName() - New security-critical function with no tests. Edge cases to cover:

    • Empty string (valid)
    • Max length (63 chars) vs 64 chars
    • Starting with digit (should fail)
    • Special characters, spaces, hyphens (should fail)
    • SQL injection attempts like '; DROP TABLE--
    • Unicode characters
  • NewPostgresIdentityStore() error handling - Constructor now returns error, but no tests verify:

    • Invalid schema names are rejected
    • Valid schema names are accepted
    • Default tenant name fallback still works
  • Error handling in ResolveOrProvisionUser - Changed from pgx.ErrNoRows to string comparison err.Error() != "no rows in result set". This is fragile and untested:

    • What if the error message changes?
    • Consider using errors.Is() or sqlc's :batchone with proper error types
  • firstTenantOrCreate() - Complex logic with multiple failure paths, no coverage for:

    • Tenant creation failure scenarios
    • Race conditions (two concurrent calls creating duplicate tenants)

⚠️ Test Quality:

  • Fragile error detection: Using err.Error() != "no rows in result set" instead of proper error type checking. Consider defining a custom error or using errors.Is() pattern.

  • Generated code testing: While sqlc generates correct code, the integration between PostgresIdentityStore and the generated queries should have integration tests.

✅ Good Coverage:

  • Existing handler_test.go tests for resolveSubjectFromClaims remain intact
  • Type safety improvements from sqlc reduce runtime SQL errors
  • sqlc-generated code is validated by sqlc's own test suite

Recommendation

Consider adding unit tests for at least validateSchemaName() and the constructor error handling before merging. Integration tests for the full user provisioning flow would be ideal but could be a follow-up.

Copy link
Owner Author

@kusold kusold left a comment

Choose a reason for hiding this comment

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

📚 Documentation Review

Summary

The PR description is excellent with comprehensive documentation of changes, benefits, and migration path. However, the public Go APIs in this PR lack godoc documentation, which is important for a framework like gotchi that will be used by other developers.

Findings

🔴 Missing Docs:

  • PostgresStoreConfig - Struct fields Schema and DefaultTenantName lack documentation
  • PostgresIdentityStore - Struct and its fields (pool, queries, cfg) are undocumented
  • NewPostgresIdentityStore - No godoc explaining what it does, when to use it, or what errors it returns (now returns error)
  • ResolveOrProvisionUser - Public method lacks godoc explaining behavior (resolves existing user or creates new one)
  • ListMemberships - No godoc explaining parameters and return values
  • GetTenantDisplay - No godoc for this public method
  • firstTenantOrCreate / createMembership - Private helper methods could use brief comments

⚠️ Unclear:

  • Error comparison pattern err.Error() != "no rows in result set" is fragile (string comparison). Consider using pgx.ErrNoRows or adding a comment explaining why this pattern is used.

✅ Well Documented:

  • PR description - Excellent comprehensive documentation
  • queries/auth.sql - sqlc query names are self-documenting (GetUserByIdentifier, InsertUser, etc.)
  • schema/auth.sql - Schema is clear and self-documenting
  • validateSchemaName - Has helpful comment explaining validation rules
  • Generated code (internal/db/*.go) - Appropriately marked as generated, no additional docs needed
  • internal/db/schema_aware.go - Has function-level comments for public API

Recommendation

Add godoc comments to the public types and methods in auth/postgres_store.go. This is especially important for a framework where users will interact with these APIs directly.

kusold added 2 commits March 24, 2026 18:10
- Replace fragile string-based error comparison with proper pgx.ErrNoRows check
- Add errors import for proper error handling with wrapped errors
- Remove dead code in internal/db/schema_aware.go with weaker validation

Security improvements:
- Use errors.Is() for proper error comparison instead of string matching
- Eliminate potential issues from string-based error checks
- Remove duplicate validation function with weaker security
Add comprehensive tests for the schema name validation function to
prevent SQL injection attacks via dynamic schema names.

Test cases cover:
- Valid schemas (empty, alphanumeric, underscores, max length)
- Invalid schemas (starts with digit, special chars, SQL injection)
- Edge cases (hyphens, spaces, dots, too long)

Addresses testing review finding: missing tests for validateSchemaName().

Note: Integration tests for NewPostgresIdentityStore() and firstTenantOrCreate()
require test database infrastructure - deferred for future PR.
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