/DB

Column Defaults

All mechanisms for specifying PostgreSQL DEFAULT expressions in Socigy.OpenSource.DB v0.1.82 — C# initializers, [Default], DbDefaults constants, and how defaults interact with ExcludeAutoFields().

updated 3 May 20268 min readv0.1.82

Overview

Socigy.OpenSource.DB provides three layered mechanisms for column defaults. Understanding all three — and how they interact — prevents the most common DDL and INSERT surprises.

  1. C# property initializer — the migration CLI reads the initializer value and emits a DEFAULT expression automatically.
  2. [Default] with no argument — flags the column as having a DB-side default without emitting a DDL expression (used with [AutoIncrement] and similar).
  3. [Default("expr")] or [Default(DbDefaults.Constant)] — emits an explicit DEFAULT expression in the DDL.

Section 1: C# Initializer as Default

NOTE
This is the feature most developers miss. You do not need [Default] for simple literal defaults.

When a property carries no [Default] attribute, the migration CLI inspects the C# property initializer. A non-zero, non-empty initializer is emitted directly as the DEFAULT expression in DDL.

[Table("courses")]
public partial class Course
{
    [PrimaryKey, Default(DbDefaults.Guid.Random)]
    public Guid Id { get; set; }

    // No [Default] — CLI reads the initializer "DEFAULT NAME"
    public string Name { get; set; } = "DEFAULT NAME";

    public int Priority { get; set; } = 5;

    public bool IsPublished { get; set; } = true;
}

Generated DDL:

CREATE TABLE "courses" (
    "id"           UUID      NOT NULL DEFAULT gen_random_uuid(),
    "name"         TEXT      NOT NULL DEFAULT 'DEFAULT NAME',
    "priority"     INTEGER   NOT NULL DEFAULT 5,
    "is_published" BOOLEAN   NOT NULL DEFAULT TRUE,
    PRIMARY KEY ("id")
);

What Produces a DEFAULT and What Does Not

C# initializer DDL DEFAULT emitted
= "Hello" DEFAULT 'Hello'
= 42 DEFAULT 42
= true DEFAULT TRUE
= MyEnum.Value DEFAULT 0 (underlying integer)
= MyDbEnum.Value DEFAULT 'Value' (PostgreSQL enum string — see below)
= 0 (ignored — zero value)
= false (ignored — zero value)
= "" (ignored — empty string)
= null (ignored)
= Guid.Empty (ignored)

Zero-values are the C# default for every value type, so the CLI cannot distinguish "you intentionally wrote = 0" from "you left the initializer at the default". Always use [Default(DbDefaults.Number.Zero)] when you want an explicit DEFAULT 0 in DDL.

Enum initializers

The behavior depends on whether the enum carries [DatabaseEnum]:

Regular enum (stored as INTEGER) — the initializer is emitted as the underlying integer:

public enum VersionState { Draft, Published, Archived }

[Table("project_versions")]
public partial class ProjectVersion
{
    // Emits: DEFAULT 0
    public VersionState State { get; set; } = VersionState.Draft;
}

Generated DDL fragment:

"state" integer DEFAULT 0

[DatabaseEnum] enum (stored as a PostgreSQL ENUM type) — the initializer is emitted as the quoted member name:

[DatabaseEnum]
public enum Priority { Low, Normal, High }

[Table("tasks")]
public partial class Task
{
    // Emits: DEFAULT 'Normal'
    public Priority Priority { get; set; } = Priority.Normal;
}

Generated DDL fragment:

"priority" priority DEFAULT 'Normal'
NOTE
The zero-value rule still applies to enums. = VersionState.Draft is only emitted as a DEFAULT if Draft is not the first member (i.e. not 0). If Draft = 0, the initializer is treated as "no default" and no DEFAULT clause is emitted. Use [Default("0")] or [Default(DbDefaults.Number.Zero)] to force an explicit DEFAULT 0 in that case.
NOTE
Combined flag values — = Test.First | Test.Fourth — are silently ignored and produce no DEFAULT clause. A combined value such as 9 is not a valid single row reference in the enum's database table. Use [Default("9")] to emit it verbatim if you intentionally store bitmasks in the column.

Section 2: Naked `[Default]` — Flag Only, No DDL Expression

[Default] with no argument tells the framework that this column has a database-side default, but it does not emit any DDL expression. The column is still marked so that ExcludeAutoFields() skips it during INSERT.

This form is used with [AutoIncrement] (sequence-assigned values) or with a primary key whose value is generated outside the application:

[Table("user_login")]
public partial class UserLogin
{
    // Naked [Default] — no DDL expression emitted.
    // ExcludeAutoFields() will skip this column on INSERT.
    [PrimaryKey, Default]
    public Guid Id { get; set; }

    public string Username { get; set; }
    public string? PasswordHash { get; set; }
}

The resulting DDL column is "id" UUID NOT NULL — no DEFAULT clause — because no expression was given. The application is expected to supply or assign the value through some other means (a trigger, an application-side call, etc.).

TIP
If you want the database to generate the UUID automatically, use [Default(DbDefaults.Guid.Random)] instead of bare [Default].

Section 3: `[Default("expr")]` — Raw SQL Expression

Pass a raw SQL expression as a string to emit it verbatim as the DEFAULT clause:

[Default("'pending'")]
public string Status { get; set; }

[Default("ARRAY[]::TEXT[]")]
public string[] Tags { get; set; } = [];

[Default("(CURRENT_DATE + INTERVAL '30 days')")]
public DateTime ExpiresAt { get; set; }

Generated DDL fragments:

"status"     TEXT      NOT NULL DEFAULT 'pending',
"tags"       TEXT[]    NOT NULL DEFAULT ARRAY[]::TEXT[],
"expires_at" TIMESTAMP NOT NULL DEFAULT (CURRENT_DATE + INTERVAL '30 days')
WARNING
The string you pass is inserted verbatim. You are responsible for correct SQL syntax. Strings must include their own single quotes (e.g. "'pending'", not "pending").

Section 4: `DbDefaults` Built-in Constants

DbDefaults provides named constants for the most common default expressions. Using these instead of raw strings makes your code DB-engine-agnostic and protected against typos.

Constant DDL expression emitted
DbDefaults.Guid.Random DEFAULT gen_random_uuid()
DbDefaults.Guid.Sequential DEFAULT uuid_generate_v1mc()
DbDefaults.Time.Now DEFAULT timezone('utc', now())
DbDefaults.Time.NowLocal DEFAULT now()
DbDefaults.Time.Date DEFAULT current_date
DbDefaults.Bool.True DEFAULT TRUE
DbDefaults.Bool.False DEFAULT FALSE
DbDefaults.Number.Zero DEFAULT 0
DbDefaults.Number.One DEFAULT 1
DbDefaults.Text.Empty DEFAULT ''

Usage:

[Table("test_items")]
public partial class TestItem
{
    [PrimaryKey, Default(DbDefaults.Guid.Random)]
    public Guid Id { get; set; }

    public string Name { get; set; }

    [Default(DbDefaults.Time.Now)]
    public DateTime CreatedAt { get; set; }

    [Default(DbDefaults.Bool.True)]
    public bool IsActive { get; set; }

    [Default(DbDefaults.Number.Zero)]
    public int RetryCount { get; set; }
}

Generated DDL:

CREATE TABLE "test_items" (
    "id"          UUID      NOT NULL DEFAULT gen_random_uuid(),
    "name"        TEXT      NOT NULL,
    "created_at"  TIMESTAMP NOT NULL DEFAULT timezone('utc', now()),
    "is_active"   BOOLEAN   NOT NULL DEFAULT TRUE,
    "retry_count" INTEGER   NOT NULL DEFAULT 0,
    PRIMARY KEY ("id")
);
NOTE
uuid_generate_v1mc() (used by DbDefaults.Guid.Sequential) requires the uuid-ossp extension. Run CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; in your database before applying migrations that use this constant.

Section 5: `ExcludeAutoFields()` — How Defaults Affect Inserts

The INSERT builder's ExcludeAutoFields() method skips any property that carries a [Default] attribute in any form. This allows the database to apply the DEFAULT expression instead of receiving an explicit value from the application.

// Id has [Default(DbDefaults.Guid.Random)] — excluded by ExcludeAutoFields()
// CreatedAt has [Default(DbDefaults.Time.Now)] — excluded by ExcludeAutoFields()
// Name has no [Default] — included
var item = new TestItem { Name = "Hello" };

await item.Insert()
    .WithConnection(conn)
    .ExcludeAutoFields()   // skips Id and CreatedAt
    .ExecuteAsync();

// Resulting SQL (approximate):
// INSERT INTO "test_items" ("name") VALUES (@name)
// The DB fills "id" and "created_at" from their DEFAULT expressions.

The naked [Default] (no argument) form is specifically designed for [AutoIncrement] columns where the sequence generates the value:

[Table("test_counters")]
public partial class TestCounter
{
    [PrimaryKey]
    public Guid Id { get; set; }

    [AutoIncrement]          // implies the column has a DB-side sequence default
    public int Seq { get; set; }

    public string Label { get; set; }
}

var counter = new TestCounter { Id = Guid.NewGuid(), Label = "first" };

// Seq is excluded because [AutoIncrement] marks it as having a DB default.
// The sequence nextval() expression in DDL assigns the value.
await counter.Insert()
    .WithConnection(conn)
    .ExecuteAsync();

Reading Back the DB-Assigned Value

Use WithValuePropagation() to execute the INSERT with RETURNING * and populate the instance's excluded fields from the returned row:

var counter = new TestCounter { Id = Guid.NewGuid(), Label = "VP" };

await counter.Insert()
    .WithConnection(conn)
    .WithValuePropagation()
    .ExecuteAsync();

// counter.Seq is now populated with the sequence value assigned by PostgreSQL
Console.WriteLine(counter.Seq);  // e.g. 42

Section 6: `[Default]` vs C# Initializer — Precedence

When both a [Default] attribute and a C# initializer are present on the same property, the [Default] attribute always wins. The initializer is ignored for DDL purposes.

// [Default] wins — DDL emits DEFAULT gen_random_uuid(), not DEFAULT 'some-fixed-guid'
[Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; } = Guid.Parse("00000000-0000-0000-0000-000000000001");

// [Default] wins — DDL emits DEFAULT timezone('utc', now()), not DEFAULT '2020-01-01'
[Default(DbDefaults.Time.Now)]
public DateTime CreatedAt { get; set; }

The C# initializer is only consulted when no [Default] attribute is present at all.

Complete Example

using Socigy.OpenSource.DB.Attributes;

[Table("orders")]
public partial class Order
{
    // DbDefaults constant — gen_random_uuid()
    [PrimaryKey, Default(DbDefaults.Guid.Random)]
    public Guid Id { get; set; }

    // C# initializer — DEFAULT 'pending'
    public string Status { get; set; } = "pending";

    // C# initializer — DEFAULT 1
    public int Priority { get; set; } = 1;

    // C# initializer — DEFAULT TRUE
    public bool RequiresShipping { get; set; } = true;

    // C# initializer on a regular enum — DEFAULT 1 (underlying integer of Urgent)
    // (assumes: public enum OrderUrgency { Normal, Urgent, Critical })
    public OrderUrgency Urgency { get; set; } = OrderUrgency.Urgent;

    // DbDefaults constant — DEFAULT timezone('utc', now())
    [Default(DbDefaults.Time.Now)]
    public DateTime CreatedAt { get; set; }

    // Raw SQL expression — DEFAULT (CURRENT_DATE + INTERVAL '7 days')
    [Default("(CURRENT_DATE + INTERVAL '7 days')")]
    public DateTime DueDate { get; set; }

    // Naked [Default] — no DDL expression; skipped by ExcludeAutoFields()
    [Default]
    public int InternalToken { get; set; }

    public string? Notes { get; set; }
}