Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infra/docker/backend-dotnet.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ COPY --from=frontend-build /app/src/frontend/build /app/wwwroot
# externally (e.g. golang-migrate run by a Helm pre-job).
COPY src/backend/clickhouse/migrations /app/clickhouse-migrations

# Postgres analytics migration files (HOL-26) — applied at startup by
# PostgresMigrationService when the Postgres analytics backend is enabled.
# Disable via PostgresAnalytics__Migrations__Disabled=true.
COPY src/dotnet/src/HoldFast.Data.Postgres/Migrations /app/postgres-analytics-migrations

# Default port — matches Go backend convention
ENV ASPNETCORE_URLS=http://+:8082
EXPOSE 8082
Expand Down
1 change: 1 addition & 0 deletions src/dotnet/HoldFast.Backend.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<Project Path="src/HoldFast.Analytics/HoldFast.Analytics.csproj" />
<Project Path="src/HoldFast.Api/HoldFast.Api.csproj" />
<Project Path="src/HoldFast.Data.ClickHouse/HoldFast.Data.ClickHouse.csproj" />
<Project Path="src/HoldFast.Data.Postgres/HoldFast.Data.Postgres.csproj" />
<Project Path="src/HoldFast.Data/HoldFast.Data.csproj" />
<Project Path="src/HoldFast.Domain/HoldFast.Domain.csproj" />
<Project Path="src/HoldFast.GraphQL.Private/HoldFast.GraphQL.Private.csproj" />
Expand Down
1 change: 1 addition & 0 deletions src/dotnet/src/HoldFast.Api/HoldFast.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<ProjectReference Include="..\HoldFast.Domain\HoldFast.Domain.csproj" />
<ProjectReference Include="..\HoldFast.Data\HoldFast.Data.csproj" />
<ProjectReference Include="..\HoldFast.Data.ClickHouse\HoldFast.Data.ClickHouse.csproj" />
<ProjectReference Include="..\HoldFast.Data.Postgres\HoldFast.Data.Postgres.csproj" />
<ProjectReference Include="..\HoldFast.GraphQL.Public\HoldFast.GraphQL.Public.csproj" />
<ProjectReference Include="..\HoldFast.GraphQL.Private\HoldFast.GraphQL.Private.csproj" />
<ProjectReference Include="..\HoldFast.Worker\HoldFast.Worker.csproj" />
Expand Down
16 changes: 16 additions & 0 deletions src/dotnet/src/HoldFast.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ req.RequestUri is null ||
builder.Configuration.GetSection("ClickHouse:Migrations"));
builder.Services.AddHostedService<ClickHouseMigrationService>();

// ── Postgres analytics (HOL-26 scaffolding) ──────────────────────────
// Companion analytics backend; lives alongside ClickHouse rather than
// replacing it (the Storage:Analytics switch in HOL-34 chooses one at
// runtime). For HOL-26 the PG migration runner is the only thing wired
// up — its job is to ensure the analytics schema + schema_migrations
// table exist on a fresh Postgres so HOL-29+ implementations have a
// place to add their per-domain tables.
//
// Disabled by default so existing deployments don't get extra startup
// work; opt in via PostgresAnalytics__Migrations__Disabled=false.
builder.Services.Configure<HoldFast.Data.Postgres.PostgresAnalyticsOptions>(
builder.Configuration.GetSection("PostgresAnalytics"));
builder.Services.Configure<HoldFast.Data.Postgres.PostgresAnalyticsMigrationOptions>(
builder.Configuration.GetSection("PostgresAnalytics:Migrations"));
builder.Services.AddHostedService<HoldFast.Data.Postgres.PostgresMigrationService>();

// ── Storage ───────────────────────────────────────────────────────────
builder.Services.Configure<StorageOptions>(
builder.Configuration.GetSection("Storage"));
Expand Down
6 changes: 6 additions & 0 deletions src/dotnet/src/HoldFast.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
"ReadonlyUsername": "default",
"ReadonlyPassword": ""
},
"PostgresAnalytics": {
"Schema": "analytics",
"Migrations": {
"Disabled": false
}
},
"Storage": {
"Type": "filesystem",
"FilesystemRoot": "/tmp/holdfast-storage"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
HoldFast.Data.Postgres — Postgres + TimescaleDB analytics backend.
Companion to HoldFast.Data.ClickHouse; users pick one at deploy time
via the Storage:Analytics config switch (HOL-34).

Phase 1 (HOL-26): scaffolding only — project, options, migration runner,
foundational migrations (analytics schema + schema_migrations tracking).
Phase 2 (HOL-29..33): per-domain IXxxStore implementations.

Dependencies are deliberately minimal:
- HoldFast.Analytics (the seam — interfaces + DTOs)
- Npgsql (the only real external dep)
- Microsoft.Extensions.* abstractions for DI/hosting
No EF Core in the analytics path — the relational HoldFast.Data project
owns that. Keeping the analytics layer query-direct avoids pulling EF
overhead into the high-volume insert path.
-->
<ItemGroup>
<ProjectReference Include="..\HoldFast.Analytics\HoldFast.Analytics.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Npgsql" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="HoldFast.Data.Tests" />
</ItemGroup>

<!-- Migration files are bundled with the published image; runtime path
defaults to /app/postgres-analytics-migrations (see backend-dotnet
Dockerfile + PostgresAnalyticsMigrationOptions.Path). -->
<ItemGroup>
<None Include="Migrations\*.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- HOL-26: Postgres analytics schema bootstrap.
--
-- The PostgresMigrationService.EnsureSchemaAsync method already creates this
-- schema before applying any migration (so the migration runner itself can
-- insert into analytics.schema_migrations). This file exists so the schema
-- creation is also recorded in the migration history — operators reading
-- *.up.sql get a complete schema-of-record story without needing to know
-- about the runner's bootstrap step.
CREATE SCHEMA IF NOT EXISTS analytics;

COMMENT ON SCHEMA analytics IS
'HoldFast analytics tables (logs, traces, sessions, errors, metrics). '
'Owned by HoldFast.Data.Postgres. Separate from public schema which '
'holds the relational data managed by HoldFast.Data + EF Core.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- HOL-26: Migration tracking table.
--
-- PostgresMigrationService.EnsureMigrationsTableAsync also creates this table
-- before applying any migration; this file exists for the same documentary
-- reason as 0001 — *.up.sql files together describe the full schema state.
--
-- Schema parity with golang-migrate's postgres driver (version + dirty),
-- plus an applied_at column for operator convenience.
CREATE TABLE IF NOT EXISTS analytics.schema_migrations
(
version BIGINT PRIMARY KEY,
dirty BOOLEAN NOT NULL DEFAULT false,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

COMMENT ON TABLE analytics.schema_migrations IS
'HOL-26: tracks applied analytics-schema migrations. '
'dirty=true rows indicate a partial-failure mid-migration that needs '
'manual investigation (the runner uses transactional DDL so this should '
'be rare).';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- HOL-26: install Postgres extensions used by the analytics schema.
--
-- Both extensions are conditional — CREATE EXTENSION IF NOT EXISTS is a
-- no-op when already installed, and the surrounding DO blocks let us
-- gracefully no-op when the extension isn't available in the running PG
-- image (e.g. vanilla `postgres:16` doesn't ship TimescaleDB; the hobby
-- compose uses `ankane/pgvector` which doesn't either).
--
-- When the extensions ARE present (e.g. `timescale/timescaledb-ha`), HOL-29+
-- migrations will use them for hypertable partitioning + retention.
-- When absent, HOL-29+ falls back to native PG partitioning via pg_partman
-- (also conditional) or per-month declarative partitions managed in-app.
--
-- Reference: docs.timescale.com/self-hosted/latest/install/installation-docker

DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'timescaledb') THEN
CREATE EXTENSION IF NOT EXISTS timescaledb;
RAISE NOTICE 'HOL-26: TimescaleDB extension enabled';
ELSE
RAISE NOTICE
'HOL-26: TimescaleDB extension not available in this PG image. '
'Analytics tables will use native partitioning. '
'For larger deployments switch the postgres image to '
'timescale/timescaledb-ha and re-run migrations.';
END IF;
END
$$;

DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pg_partman') THEN
CREATE EXTENSION IF NOT EXISTS pg_partman;
RAISE NOTICE 'HOL-26: pg_partman extension enabled';
ELSE
RAISE NOTICE
'HOL-26: pg_partman not available. Retention will fall back to '
'in-app DELETE WHERE timestamp < … instead of partition drops.';
END IF;
END
$$;
51 changes: 51 additions & 0 deletions src/dotnet/src/HoldFast.Data.Postgres/PostgresAnalyticsOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace HoldFast.Data.Postgres;

/// <summary>
/// Configuration for the Postgres analytics backend.
///
/// HOL-26: HoldFast's hobby deployment runs a single Postgres container that
/// holds both the relational data (users, projects, workspaces — owned by
/// HoldFast.Data via EF Core) AND the analytics data (logs, traces, sessions,
/// errors, metrics — owned by HoldFast.Data.Postgres via direct Npgsql).
/// The two share a host but live in separate schemas (relational uses
/// `public`; analytics uses `analytics`).
///
/// Production deployments can point AnalyticsConnectionString at a separate
/// Postgres instance for capacity isolation; HoldFast doesn't care.
/// </summary>
public class PostgresAnalyticsOptions
{
/// <summary>
/// Npgsql connection string for the analytics database. If unset, falls
/// back to ConnectionStrings:PostgreSQL (the relational connection),
/// which is the right default for the hobby/lean stack where one PG
/// container hosts both schemas.
/// </summary>
public string? ConnectionString { get; set; }

/// <summary>
/// Schema name for analytics tables. Default `analytics` keeps the
/// relational `public` schema clean.
/// </summary>
public string Schema { get; set; } = "analytics";
}

/// <summary>
/// Configuration for the Postgres analytics migrations runner.
/// </summary>
public class PostgresAnalyticsMigrationOptions
{
/// <summary>
/// Filesystem path to the directory containing *.up.sql migration files.
/// In the Docker image these are copied to /app/postgres-analytics-migrations.
/// Locally (e.g. during dev runs) point this at
/// src/dotnet/src/HoldFast.Data.Postgres/Migrations.
/// </summary>
public string Path { get; set; } = "/app/postgres-analytics-migrations";

/// <summary>
/// Skip running migrations. Useful when the analytics schema is managed
/// externally (Helm pre-jobs, external golang-migrate runs, etc).
/// </summary>
public bool Disabled { get; set; }
}
Loading
Loading