Skip to content

Data Migration

View .md

A migration job reads rows from a source through the same read-only path every Beacon query uses, then writes them into a table of a destination database. It is the only Beacon feature that writes to a connected database, so it is off unless the host turns it on, and it writes only into a data source an Admin marked for it.

What a job does:

  • reads one source query (plain SQL, or a JSON list of steps across data sources) within its own row budget;
  • writes the rows into one destination table in one of three modes: Insert only, Insert or update (Upsert) or Truncate & insert;
  • records every run in its history: rows read, written and failed, duration and error.

Typical uses: copy reference data into an analytics database, keep a reporting table refreshed, move rows between PostgreSQL, MySQL and SQL Server.

Data migration is off by default. The host turns it on in configuration:

{
"Beacon": {
"Migrations": {
"Enabled": true,
"MaxRows": 100000
}
}
}
KeyDefaultWhat it does
Beacon:Migrations:EnabledfalseDeployment lock. While false, nobody can create or run a job, an Admin included.
Beacon:Migrations:MaxRows100000Row budget of a migration’s source read (1 to 10,000,000), used in place of Beacon:Query:MaxExecutionRows.

While Enabled is false:

  • the /beacon/api/migrations/* routes are not mapped (every one answers 404), and the UI shows no Data Migration entry, page or Home card;
  • the migration handlers refuse every request (409), also for an in-process caller that does not use the routes;
  • no data source can be marked as a migration write target (409). An existing target can still be returned to a read source.

See Configuration.

A job writes only into a data source an Admin marked as a migration write target: on the data source’s page, choose Mark as migration write target, or call PUT /beacon/api/data-sources/{id}/migration-writes (Admin only).

  • Only a PostgreSQL, MySQL or SQL Server database can be a write target. A host-managed data source or one marked read-only cannot.
  • Beacon never reads a write target. Query steps, previews, subscriptions, data quality, MCP tools, AI grounding, documentation and metadata scans refuse it or leave it out. Only the migration writer connects with its credentials, so only its login holds write grants; every data source Beacon reads keeps a read-only login.
  • A job’s declared source and the data source of every step it runs may not be a write target. To copy within one database, register it twice: once with a read-only login (the source) and once with the write login (the target).
  • Marking a data source that live queries, subscriptions or data contracts read is refused (409, with the count of each) unless the Admin confirms it ("force": true; the page asks a second time). Those readers fail once Beacon stops reading the source.
  • Returning a target to a read source asks for confirmation: Beacon reads it again with its stored login, so change that login to a read-only one first.
  • Every change is recorded in the settings history (DataSource.{id}.AllowMigrationWrites, before and after, and who) and as the MigrationWritesChanged security log event (7115, ids only).

The flag is checked when a job is created, before every run, and by the writer before it decrypts the destination’s credentials. A run started from the page or the API whose destination is not (or no longer) a write target, or that reads one, is refused with the reason (400). A run refused inside the migration service itself (an in-process caller, or a flag changed between the two checks) is recorded in the job’s history as failed, with the reason.

See Data Sources for the data source side.

Data migration needs the MigrationOperator permission (an Admin holds every permission; see Security model). Listing jobs, opening one, creating, running and deleting jobs and reading the run history all need it. The Data Migration entry is hidden from other users.

A MigrationOperator gets no bypass of project access. A job reads its declared source and the data source of every step in its query. Creating it, every run of it and deleting it need:

  • Query on each of those data sources and Write on the destination, through the projects that link them (404 for a data source the caller cannot read, 403 for one it reads but may not query or write);
  • a destination that is no more widely reachable than any source: every live project that links the destination must link each source too, and an openly reachable destination needs openly reachable sources (403 otherwise). Copying data never makes it readable to more people.

The job list, a job and the run history show a MigrationOperator only jobs whose source and destination they can read (another job is not found, 404). A failed run’s error text, which can quote any data source the job reads, is replaced by a note unless the caller reads every one of them. An Admin is not checked. Refusals are logged with ids only.

The Home page’s migration summary shows Admins the totals of every job, MigrationOperators the totals of the jobs their list shows, and zeros to everyone else.

  1. Open Data Migration (/beacon/migration-jobs).
  2. Click New job.
  3. Fill in the form and click Create migration job.
FieldRequiredDefaultWhat it does
Job nameYesUp to 200 characters.
DescriptionYesUp to 1,000 characters.
Source data sourceYesThe data source the job reads. Cannot be a write target.
Source SQLYesThe source query. See Source query.
Destination data sourceYesLists migration write targets only.
Destination tableYesFor example public.target_table.
Migration modeYesInsert onlyInsert only, Insert or update or Truncate & insert. See Modes.
Cron scheduleNoemptyStandard 5-field cron, stored with the job. See Schedules.
Max retriesNo30 to 10. Stored with the job and shown on each run as Retry. A failed run is not retried automatically.
Timeout (minutes)No301 to 1,440. Stored with the job.
Enable migration jobNoonA disabled job does not run.
Validate query before executionNoonStored with the job.
Transformation script (optional)NoemptyLeave it empty. A job with a script fails when it runs.

The job is saved when the form is valid; its SQL is not run at creation. The destination table is checked for existence on every run.

The source is read the way every query runs (see Queries): through the read-only execution gate and the source’s read-only path, within Beacon:Query:StatementTimeoutSeconds (default 120 seconds) per statement.

  • Row budget. Beacon:Migrations:MaxRows replaces Beacon:Query:MaxExecutionRows. The cap is written into the source SQL and the read stops one row past it. A larger source fails the run, with a message naming Beacon:Migrations:MaxRows, before anything is written. Split the job (for example by date or key range) or raise the budget. A run holds every row it copies in memory, so size the budget to the host.
  • Failed reads write nothing. If any step or the final query fails or times out, the run is recorded as failed and the destination is not opened. A Truncate-mode job never empties its table on a failed read.
  • Columns map by name. The source’s column names are used as the destination column names.
  • Parameters. A run from Run now or POST /beacon/api/migrations/jobs/{id}/run passes no parameter values.

Several data sources in one job. The job’s query text can also be a JSON array of steps, each naming its own data source. The form takes a single SQL statement; send the JSON form through POST /beacon/api/migrations/jobs:

[
{ "Name": "Users", "DataSourceId": 3, "SqlValue": "SELECT user_id, email FROM users WHERE active = true" },
{ "Name": "Orders", "DataSourceId": 7, "SqlValue": "SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id" }
]

Each step is a read on its own data source, and the caller needs Query on each. Results of several data sources are joined in an in-memory SQLite database, as for any multi-step query. Anything that is not a JSON array is read as one step of plain SQL.

Mode (UI label)What it doesNeeds
Insert onlyInserts every source row. A row that violates a key or constraint fails the write.
Insert or update (Upsert)Stages the rows in a temporary table, then inserts new rows and updates existing ones by primary key.A primary key on the destination table, and every key column in each source row (otherwise the run fails). Without a primary key the job inserts, as in Insert only.
Truncate & insertEmpties the destination table, then inserts every source row.

How the write is done per engine:

EngineInsertUpsert
PostgreSQLCOPY … FROM STDIN (FORMAT BINARY)COPY into a temporary table, then INSERT … ON CONFLICT
SQL Serverbulk copybulk merge, staged in a session temporary table
MySQLbulk insert; if it fails, row by rowbulk insert into a temporary table, then INSERT … ON DUPLICATE KEY UPDATE

On PostgreSQL and SQL Server a failed write fails the run. On MySQL, when the bulk insert fails, Beacon inserts the rows one at a time: rows that fail are counted and listed in the run’s error, and the run ends as Partial. It stops after more than 100 failed rows.

The cron schedule is stored with the job and shown on it. Beacon does not start migration runs from it by itself: a run starts from Run now or from POST /beacon/api/migrations/jobs/{id}/run. A host that wants scheduled runs starts them from its own background job (see Background jobs); the approval and write-target checks apply to those runs too.

  1. Open the job from the jobs list.
  2. Click Run now.

Run now is disabled while the job is disabled or awaiting approval. The run happens in the request; the page then shows it in the job’s runs.

The jobs list (/beacon/migration-jobs) is paged on the server and shows Id, Name, Source → destination, Mode, Enabled, Schedule and Created, with a delete action per row. History opens the run history of all jobs.

A job’s page shows its definition (source, destination, mode, schedule, enabled, created), Status, Last run and the successes and failures among its recent runs, and its runs with Started, Status, Duration, Rows, Retry and Error.

The run history page (/beacon/migration-history) lists runs of every job you can read with Started, Job, Status, Rows, Failed and Rows/s.

StatusMeaning
RunningThe run has started.
CompletedEvery row was written.
PartialSome rows failed (MySQL row-by-row fallback); see the run’s error. The job page labels it Partial success.
FailedThe read or the write failed, or the run was refused (not approved, not a write target). The error says why.

Click the delete action on the row, or Delete on the job’s page, and confirm. The job is archived; its run history is kept.

The approval workflow is off by default; a host turns it on in code with options.EnableApprovalWorkflow(...). While it is on, its CoversMigrationJobs switch (on by default) makes every new job wait for a second person’s approval before it writes anything:

  • A new job is submitted for approval, with its creator as the requester. The create answers with awaitingApproval: true and the message “Submitted for approval: the job does not run, manually or on its schedule, until a second person approves it.” A caller Beacon cannot name (an API key without an active owner, for example) creates nothing (403) unless the host sets AllowSelfApproval.
  • An Approver (or an Admin) other than the creator reviews it on the Approvals page under Migration jobs. The review shows the definition: source and destination data sources, the source query (SQL or JSON steps), the destination table, the mode and the transformation script. The approval covers exactly that definition. Name, description, schedule, retry, timeout and enabled are not part of it. Someone without the Approver permission who reads both of the job’s data sources can open the request to see what waits, but cannot decide it.
  • No run starts before the approval. A run is refused and recorded as failed with “Not run: the job’s definition waits for approval. A second person approves it on the approvals page; until then the job does not run.”, and a Warning is logged with the job id. The job shows Awaiting approval with a link to the request, and Run now is disabled.
  • A job runs only the definition that was approved. If the stored definition differs from the approved one (a change made outside Beacon), the job does not run until a matching definition is approved.
  • A rejected job never runs. Its page shows that the definition was rejected, with the reviewer’s comment and the time of the decision (lastApprovalDecision on the job). Delete it and create the job again with a corrected definition, which is submitted again.
  • There is no withdraw action: an Approver rejects the request, or the creator deletes the job.
  • Nobody approves their own job unless the host sets AllowSelfApproval, and a request is decided once.

Only a migration write target’s login needs write grants. Grant it only what the job’s mode uses, on the destination table only:

ModePostgreSQLMySQLSQL Server
Insert onlyINSERT (written with COPY … FROM STDIN)INSERTINSERT (bulk copy)
Insert or updateINSERT, UPDATE, and TEMPORARY on the database (rows are staged in a temporary table)INSERT, UPDATE, CREATE TEMPORARY TABLESINSERT, UPDATE (staged in a session temporary table)
Truncate & insertthe TRUNCATE privilege, INSERTDROP on the table (MySQL requires it to truncate), INSERTALTER on the table (SQL Server requires it to truncate), INSERT

Every mode also reads information_schema to check that the table exists (and, for Upsert, its primary key). Some of these grants are DDL-grade (ALTER, DROP, the TRUNCATE privilege), which is why such a login belongs on a write target that Beacon never reads, not on an ordinary data source.

All routes are under /beacon/api/migrations, mapped only while Beacon:Migrations:Enabled is true, and need the MigrationOperator permission.

MethodRouteWhat it does
GET/jobsList jobs (paged: page, pageSize, sort).
GET/jobs/{id}One job.
POST/jobsCreate a job.
POST/jobs/{id}/runRun a job now.
DELETE/jobs/{id}Archive a job.
GET/executionsList runs (paged).
SymptomCheck
No Data Migration entryBeacon:Migrations:Enabled is true, and you hold MigrationOperator (or are an Admin).
The destination picker is emptyAn Admin has marked the destination as a migration write target.
Run now is disabledThe job is enabled and not awaiting approval.
Run failed on a job with a transformation scriptCreate the job again with Transformation script empty.
Run failed: row budget exceededThe source returned more than Beacon:Migrations:MaxRows rows. Split the job or raise the budget.
Run failed: table does not existThe destination table exists in the write target’s database, and the name includes its schema if needed.
Run failed: missing primary key columnsIn Upsert mode, the source query returns every primary key column of the destination table.
Run failed: permission denied on the destinationThe write target’s login has the destination privileges of the job’s mode.