Copy Targets¶
Copy targets generate the boilerplate for bulk-loading rows into a table with PostgreSQL COPY ... FROM STDIN — the fast path for "stage everything, then process server-side in one call".
Like everything in db-gen, this is language-agnostic: the tool emits metadata describing the table, and a per-language template turns it into correct COPY code (text/CSV with escaping, or binary). No language specifics live in the tool.
It also catches silent schema drift
Copy-target tables are tracked for change detection. A renamed or retyped staging column is reported by generate and database-changes instead of silently breaking your loader.
Why¶
Apps that bulk-import data tend to hand-write the same COPY plumbing per table: the column list in the right order, where the context columns (created_by, job_run_id, …) go, which columns are nullable, and the NULL sentinel. db-gen reads that from information_schema.columns and gives a template everything it needs to write it once, per language, and regenerate when the table changes.
Configuration¶
{
"GenerateCopyTargets": true,
"CopyTargetTemplate": "./templates/copy-pgx.gotmpl",
"CopyTargetsFolderName": "copy",
"CopyTargets": [
{
"Schema": "stage",
"Table": "data_to_process",
"MappedName": "StageData",
"Format": "text",
"NullString": ""
}
],
"ContextParameterMappings": [
{ "ParameterNames": ["created_by"], "ContextPath": "ctx.CreatedBy" },
{ "ParameterNames": ["job_run_id"], "ContextPath": "ctx.JobRunId" }
]
}
Files are written to OutputFolder/CopyTargetsFolderName (default copy/), named after each target's MappedName (or a generated name), using GeneratedFileExtension and GeneratedFileCase.
Context columns are matched the same way as routine context parameters — by db column name (case-insensitive) against ContextParameterMappings.
| Field | Description |
|---|---|
Schema / Table |
The table to read columns from. |
MappedName |
Override the generated struct/file name. |
Format |
Wire-format hint: csv (default), text, or binary. The template decides what to do with it. |
NullString |
NULL sentinel for text/CSV formats. |
The metadata contract¶
A template receives CopyTargetTemplateData:
type CopyTargetTemplateData struct {
Config *Config
CopyTarget CopyTarget
BuildInfo *version.BuildInformation
}
type CopyTarget struct {
Schema string
Table string
DbFullTableName string // "schema.table"
StructName string // generated type/file name (or MappedName)
Format string // "csv" | "text" | "binary" (hint)
NullString string // NULL sentinel for text/CSV
ContextColumns []Property // injected from context (created_by, job_run_id, …)
DataColumns []Property // real data columns, in ordinal order
AllColumns []Property // ContextColumns ++ DataColumns = COPY column list, in wire order
}
Each column is a Property (see Template data reference). The fields that matter for copy code:
| Field | Meaning |
|---|---|
DbColumnName |
The column name — the COPY contract speaks column names. |
DbColumnType |
PostgreSQL udt_name, for choosing a binary type or a cast. |
PropertyType |
Mapped, nullable-aware target type — useful for context-column parameters. |
Nullable |
Whether the column accepts NULL — drives the NULL-vs-empty decision. |
Position |
For data columns only: the 0-based source field index, in table-ordinal order. |
IsContextParameter / ContextPath |
Set on context columns; ContextPath is where the value comes from. |
Wire order rules
AllColumnsis the exactCOPY (col, col, …)list: context columns first, then data columns. Emit it verbatim.DataColumns[i].Position == i— data rows arrive as fields in table-ordinal order; index the incoming row byPosition.- Context values are supplied once (as parameters) and injected into every row; data values stream per row.
Example templates¶
Both reference templates take data fields as text in ordinal order and turn empty nullable fields into NULL.
var {{.CopyTarget.StructName}}Columns = []string{
{{- range .CopyTarget.AllColumns}}
"{{.DbColumnName}}", // {{.DbColumnType}}{{if .IsContextParameter}} (context: {{.ContextPath}}){{end}}
{{- end}}
}
func (s *…CopySource) Values() ([]any, error) {
return []any{
{{- range .CopyTarget.ContextColumns}}
s.{{camelCased .DbColumnName}}, // {{.DbColumnName}}
{{- end}}
{{- range .CopyTarget.DataColumns}}
s.val({{.Position}}, {{.Nullable}}), // {{.DbColumnName}} {{.DbColumnType}}
{{- end}}
}, nil
}
private const string CopySql =
"COPY {{.CopyTarget.DbFullTableName}} (" +
{{- range $i, $col := .CopyTarget.AllColumns}}
"{{if $i}}, {{end}}{{$col.DbColumnName}}" +
{{- end}}
") FROM STDIN (FORMAT BINARY)";
// per row:
{{- range .CopyTarget.ContextColumns}}
await w.WriteAsync({{camelCased .DbColumnName}}, NpgsqlDbType.Text, ct); // {{.DbColumnName}}
{{- end}}
{{- range .CopyTarget.DataColumns}}
await WriteTextOrNull(w, f[{{.Position}}], {{.Nullable}}, ct); // {{.DbColumnName}} {{.DbColumnType}}
{{- end}}
The same metadata drives an Elixir/text-CSV template (escaping + NULL '') just as well — the tool doesn't care which.
Limitations¶
- Copy targets read column metadata from
information_schemaand require a live database connection (they are not part of the offline routines file). - The example templates pass data as text and cast/stage server-side. For a strongly-typed staging table, switch the per-column writes to use
DbColumnType.