Universal SQL loader: extract from PostgreSQL, MySQL, BigQuery, or Redshift; align schema and upsert/overwrite into BigQuery, PostgreSQL, or MySQL—generated merge DDL, shared types.
Project description
Ingestra
Move data from one SQL system to another without hand-writing merge DDL. Ingestra is a small library for extract -> optional temp file step -> align schema -> upsert or overwrite across PostgreSQL, MySQL, BigQuery, and Redshift. The same DataSource + Ingest API works whether your warehouse is BigQuery or another relational database: one mental model, dialect-specific SQL generated for you.
Why teams adopt it fast
- One pipeline shape everywhere: stop rewriting ingestion logic for each engine pair.
- Schema drift handled for you: target
create/alterfollows source columns so migrations are less brittle. - Merge behavior you control: choose
upsertoroverwrite, with lifecycle rules for target-only rows. - Cloud and relational friendly: local NDJSON temp files for relational targets, cloud temp-load prep for BigQuery/Redshift.
- Practical for real ETL:
dry_runfor safe SQL previews,verbosefor operational debugging.
The universal pattern
extract -> schema align -> delta prep -> upsert/overwrite
Keep this sequence, and you get predictable loads regardless of where data starts and where it lands.
Install
pip install .
pip install ".[postgres]" # or mysql, bigquery, all
Run tests:
python3 -m unittest discover -s ingestra/tests -p "test_*.py" -v
Core Concepts
DataSource: source or target implementation (columns,extract,extract_delta, optional target merge/schema methods).Ingest: orchestrates source + target and mirrorsdry_run/verboseto both.*_delta: helper table used only to prepare one load before moving rows into the final table.- PostgreSQL/MySQL/Redshift: physical heap table.
- BigQuery: external table over cloud storage files.
LifecycleRule: controls target-only rows foroverwrite(Delete,MarkAsDeleted,KeepUntouched).
Purpose of *_delta (very simple):
- Put new rows in
*_deltafirst. - Compare
*_deltawith the final table. - Insert/update/delete in the final table based on your merge mode.
- Reuse
*_deltafor the next run.
It exists so loads are safer and easier to reason about than writing directly into the final table first.
Recommended Order of Operations
Local Targets (PostgreSQL, MySQL)
- Extract (
extract_to_local_fileorextract_delta_to_local_file). - Align target schema (
create_or_update_target_schema). - Load target delta from file (
load_target_delta_from_ndjson_file). - Merge (
upsertoroverwrite).
Cloud Targets (BigQuery, Redshift)
- Extract (to local file, or source-to-cloud when supported).
- Align target main schema (
create_or_update_target_schema). - Prepare target delta from cloud storage (
prepare_target_delta_from_cloud_storage). - Merge (
upsertoroverwrite).
Quick routing
- If target is
PostgreSQL/MySQL: use the local target flow. - If target is
BigQuery/Redshift: use the cloud target flow. - In both cases, extraction completes first; merge-side work starts after that.
Flow A: PostgreSQL -> BigQuery
import datetime
import ingestra
source = ingestra.PostgreSQL("orders", schema_name="public")
source.connection(
host="db.example.com",
user="readonly",
password="...",
database="app",
port=5432,
)
target = ingestra.BigQuery(
"my-gcp-project",
"staging",
"orders",
primary_keys=["order_id"],
)
target.connection("/path/to/service-account.json")
job = ingestra.Ingest(
source,
target,
deletion_rule=ingestra.KeepUntouched(), # or Delete(), MarkAsDeleted("is_deleted")
dry_run=False,
verbose=True,
)
# 1) Extract source delta to local NDJSON.
path = job.extract_delta_to_local_file(
start_time=datetime.datetime.utcnow() - datetime.timedelta(days=3),
end_time=datetime.datetime.utcnow(),
column_name="updated_at", # SQL sources require this for extract_delta
)
# 2) Upload NDJSON to GCS (outside Ingestra).
# 3) Align BigQuery main table schema.
job.create_or_update_target_schema()
# 4) Prepare BigQuery temporary loading table (`*_delta`) from GCS URIs.
job.prepare_target_delta_from_cloud_storage(
ingestra.CloudStorageDeltaSpec(
uris=["gs://bucket/orders/part-000.ndjson"],
)
)
# 5) Merge staged delta into main table.
job.upsert()
# job.overwrite()
Flow B: MySQL <-> PostgreSQL (NDJSON staged)
Both relational targets use the same pattern: extract -> schema align -> load delta -> merge.
import datetime
import ingestra
source = ingestra.MySQL("orders", schema_name="app")
source.connection(host="legacy", user="ro", password="...", database="app")
target = ingestra.PostgreSQL(
"orders",
schema_name="warehouse",
primary_keys=["order_id"], # recommended on target
)
target.connection(
host="pg.example.com",
user="etl",
password="...",
database="analytics",
port=5432,
)
job = ingestra.Ingest(source, target, deletion_rule=ingestra.Delete(), verbose=True)
# 1) Extract batch.
path = job.extract_delta_to_local_file(
start_time=datetime.datetime.utcnow() - datetime.timedelta(days=1),
end_time=datetime.datetime.utcnow(),
column_name="updated_at",
)
# 2) Align target schema.
job.create_or_update_target_schema()
# 3) Load NDJSON into target temporary loading table (`*_delta`).
job.load_target_delta_from_ndjson_file(path)
# 4) Merge into main table.
job.overwrite()
Swap source/target classes for PostgreSQL -> MySQL; flow is unchanged.
Flow C: PostgreSQL -> Redshift (cloud staged)
import datetime
import ingestra
source = ingestra.PostgreSQL("orders", schema_name="public")
source.connection(host="db.example.com", user="readonly", password="...", database="app", port=5432)
target = ingestra.Redshift(
"orders",
schema_name="analytics",
primary_keys=["order_id"], # merge keys (no PK DDL assumptions)
)
target.connection(
host="redshift.example.com",
user="etl",
password="...",
database="warehouse",
port=5439,
)
job = ingestra.Ingest(source, target, deletion_rule=ingestra.Delete(), verbose=True)
# 1) Extract source delta.
path = job.extract_delta_to_local_file(
start_time=datetime.datetime.utcnow() - datetime.timedelta(days=1),
end_time=datetime.datetime.utcnow(),
column_name="updated_at",
)
# 2) Upload NDJSON to S3 (outside Ingestra).
# 3) Align Redshift main table schema.
job.create_or_update_target_schema()
# 4) Prepare Redshift temporary loading table (`*_delta`) via COPY from cloud storage.
job.prepare_target_delta_from_cloud_storage(
ingestra.CloudStorageDeltaSpec(
uris=["s3://your-bucket/orders/2026-04-20/"],
credentials=ingestra.AwsIamRoleCredentials(
iam_role="arn:aws:iam::123456789012:role/redshift-copy"
),
region="eu-west-1",
truncate=True,
)
)
# 5) Merge into Redshift main table.
job.upsert()
Redshift can also export from source tables directly with:
extract_to_cloud_storage(...)extract_delta_to_cloud_storage(...)
Both use UNLOAD and typed AWS credentials.
Flow D: Custom DataSource -> PostgreSQL (API-backed)
Use this when your source is not SQL-backed or have a custom integration.
Minimal shape:
import datetime
import ingestra
class MyApiSource(ingestra.DataSource):
def columns(self, refresh=False) -> list:
return [
ingestra.Column("id", "VARCHAR", ""),
ingestra.Column("value", "FLOAT", ""),
ingestra.Column("updated_time", "TIMESTAMP", ""),
]
def extract(self) -> list:
return self.extract_delta(
start_time=datetime.datetime.utcnow() - datetime.timedelta(days=365),
end_time=datetime.datetime.utcnow(),
)
def extract_delta(self, start_time: datetime.datetime, end_time: datetime.datetime = None) -> list:
# Fetch from API and yield normalized rows.
yield self.extract_row({"id": "a", "value": 1.23, "updated_time": "2026-04-20 00:00:00"})
source = MyApiSource()
target = ingestra.PostgreSQL("metrics", schema_name="public", primary_keys=["id"])
# target.connection(...)
job = ingestra.Ingest(source, target, deletion_rule=ingestra.KeepUntouched(), verbose=True)
# 1) Extract source delta.
path = job.extract_delta_to_local_file(
start_time=datetime.datetime.utcnow() - datetime.timedelta(days=7),
end_time=datetime.datetime.utcnow(),
)
# 2) Align target schema.
job.create_or_update_target_schema()
# 3) Load NDJSON into target temporary loading table (`*_delta`).
job.load_target_delta_from_ndjson_file(path)
# 4) Merge staged delta into main table.
job.upsert()
Important: if your columns() list expands later, create_or_update_target_schema() propagates new columns to the target main table and target *_delta temporary loading table automatically.
Ingest Shortcuts
| Method | Role | Use it for |
|---|---|---|
extract_to_local_file(output_dir_name='/tmp') |
Full extract -> NDJSON file path. | Any source -> temp-file workflow |
extract_delta_to_local_file(start_time, end_time=None, *args, **kwargs) |
Delta extract -> NDJSON file path; forwards source-specific delta args/kwargs. | Any source -> incremental temp-file workflow |
create_or_update_target_schema(**kwargs) |
Calls target create if empty, else alter. |
All targets (PostgreSQL, MySQL, BigQuery, Redshift) |
load_target_delta_from_ndjson_file(path, truncate=True, batch_size=500) |
Loads NDJSON into target temporary loading table (*_delta). |
Local targets (PostgreSQL, MySQL) |
prepare_target_delta_from_cloud_storage(delta_spec) |
Target-agnostic cloud temp-load prep. | Cloud targets (BigQuery, Redshift) |
upsert(primary_keys=None) |
Merge from temporary loading table (*_delta) without target-only delete behavior. |
Targets with merge support (PostgreSQL, MySQL, BigQuery, Redshift) |
overwrite(primary_keys=None) |
Merge from temporary loading table (*_delta) and apply deletion_rule to target-only rows. |
Targets with overwrite support (PostgreSQL, MySQL, BigQuery, Redshift) |
extract_to_cloud_storage(...) / extract_delta_to_cloud_storage(...) |
Source-side cloud export helpers. | Redshift source exports |
Typed Configuration DSL + JSON
You can also define pipeline configuration as typed objects and load/save them as JSON.
import ingestra
cfg = ingestra.IngestraConfiguration(
source=ingestra.MySQLConfiguration(
table_name="table_name",
schema_name="public",
federated_connection="source_conn",
),
target=ingestra.BigQueryConfiguration(
project_name="project_name",
dataset_name="dataset_name",
table_name="table_name",
primary_keys=["id"],
federated_connection="target_conn",
),
schedules=[
ingestra.DeltaIngestion(
frequency="hourly_3am_to_8pm",
column_name="updated_at",
lookback_seconds=7200,
deletion_rule=ingestra.KeepUntouchedRuleConfiguration(),
),
ingestra.SnapshotIngestion(
frequency="never",
deletion_rule=ingestra.DeleteRuleConfiguration(),
),
],
)
raw_json = cfg.to_json(indent=2)
cfg2 = ingestra.load_configuration_from_json(raw_json)
# Or from a JSON file path:
# cfg2 = ingestra.load_configuration_from_json_file("/path/to/config.json")
connections = {
"source_conn": mysql_connection,
"target_conn": bigquery_client,
}
ingest = cfg2.build(federated_connection=lambda name: connections[name])
Use the full-config loader above as the default path so app code has one JSON entrypoint.
JSON uses explicit typed objects (a type field) so it is deterministic to parse:
- source/target:
mysql,postgresql,bigquery,redshift - schedules:
delta,snapshot - lifecycle rules:
keep_untouched,delete,mark_as_deleted
Capabilities by Engine
| Area | PostgreSQL | MySQL | BigQuery | Redshift |
|---|---|---|---|---|
| Can export a whole table | [x] | [x] | [x] | [x] |
| Can export only new/changed rows | [x] | [x] | [x] | [x] |
| Can create/update the target table shape | [x] | [x] | [x] | [x] |
| Can load target table from cloud storage files | - | - | [x] | [x] |
| Can apply each load batch into the target table | [x] | [x] | [x] | [x] |
| Can export source data to cloud storage files | - | - | [ ] | [x] |
Legend: [x] = supported, [ ] = not supported yet (but useful), - = not applicable.
Notes
- SQL-backed
extract_deltarequires acolumn_name(for time-window filtering). dry_run=Trueprints SQL and avoids execution for supported operations.verbose=Trueprints generated SQL during execution.
License
Copyright © 2019–2026 Datale Pte. Ltd.
Distributed under the MIT License.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ingestra-0.2.3.tar.gz.
File metadata
- Download URL: ingestra-0.2.3.tar.gz
- Upload date:
- Size: 64.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8408b0b847f2eee0574188b5e4db456293a19767372587bf602bb9bcbfd5835
|
|
| MD5 |
9f2089b94d46d3c1aeaf8d28d63933f4
|
|
| BLAKE2b-256 |
fbdedf8d2bf8c32a9339d565a5790fb1ec4851d5422bdd25ab8ae7d4a51404e0
|
Provenance
The following attestation bundles were made for ingestra-0.2.3.tar.gz:
Publisher:
publish.yml on dataleio/ingestra
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ingestra-0.2.3.tar.gz -
Subject digest:
d8408b0b847f2eee0574188b5e4db456293a19767372587bf602bb9bcbfd5835 - Sigstore transparency entry: 1449812185
- Sigstore integration time:
-
Permalink:
dataleio/ingestra@d1699ffe0cabf3ef01658024e09ffe9c680fbe7e -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/dataleio
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d1699ffe0cabf3ef01658024e09ffe9c680fbe7e -
Trigger Event:
release
-
Statement type:
File details
Details for the file ingestra-0.2.3-py3-none-any.whl.
File metadata
- Download URL: ingestra-0.2.3-py3-none-any.whl
- Upload date:
- Size: 69.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
788be84c2b921340bf43f4d4109f866342b69f1903bf065d203997f685df77c4
|
|
| MD5 |
bea202612b2e6aacb4c085368dd3d7f2
|
|
| BLAKE2b-256 |
53d1a246b9bfa50f729f754695b23de6ad40415ee8fa36ac92b2d5a6e1627f54
|
Provenance
The following attestation bundles were made for ingestra-0.2.3-py3-none-any.whl:
Publisher:
publish.yml on dataleio/ingestra
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ingestra-0.2.3-py3-none-any.whl -
Subject digest:
788be84c2b921340bf43f4d4109f866342b69f1903bf065d203997f685df77c4 - Sigstore transparency entry: 1449812352
- Sigstore integration time:
-
Permalink:
dataleio/ingestra@d1699ffe0cabf3ef01658024e09ffe9c680fbe7e -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/dataleio
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d1699ffe0cabf3ef01658024e09ffe9c680fbe7e -
Trigger Event:
release
-
Statement type: