Principle:Spotify Luigi Database Data Loading
| Knowledge Sources | Spotify Luigi Repository |
|---|---|
| Domains | Pipeline_Orchestration, Database, ETL |
| Last Updated | 2026-02-10 00:00 GMT |
Overview
Database Data Loading is the process of transferring rows of processed pipeline data from an in-memory or file-based representation into a relational database table, using bulk insertion mechanisms within a single transactional boundary.
Description
After a pipeline task has transformed its input data and declared the target tables schema, the final step in the database ingestion workflow is the actual data loading -- the physical transfer of rows into the database. This principle governs how that transfer is organized, optimized, and made resilient.
Effective database data loading addresses several concerns:
- Row generation: The task produces rows of data through a generator method (commonly named
rows()) that yields tuples or lists. Each tuples elements correspond to the columns declared in the schema. This lazy generation avoids holding the entire dataset in memory. - Value mapping and escaping: Before rows are written, individual column values may need transformation -- escaping special characters (tabs, newlines, backslashes), converting
Noneto database-specific NULL representations, or casting types. Amap_column()function handles these per-value transformations. - Bulk transfer mechanism: Rather than issuing one
INSERTper row, efficient data loading uses bulk mechanisms. PostgreSQLsCOPY FROM STDINcommand reads tab-separated data from a file-like object, achieving dramatically higher throughput. MySQL uses batchedINSERTwithexecutemany(). SQLAlchemy uses chunked inserts with configurablechunk_size. - Temporary staging: For file-based bulk copy (e.g., PostgreSQL
COPY), the pipeline writes all rows to a temporary file first, then streams the file to the database. This allows progress logging and ensures the data source is seekable. - Pre-copy and post-copy hooks: The loading process supports extensible hooks that run within the same transaction, immediately before and after the data transfer. The
init_copy()hook can truncate old data or add metadata columns, whilepost_copy()can cleanse, deduplicate, or transform data in a staging table before final insertion. - Transactional integrity: The entire sequence -- init_copy, data copy, post_copy, and completion marking -- executes within a single database transaction. If any step fails, the transaction is rolled back, leaving the database in its prior consistent state.
- Automatic table creation retry: If the initial copy attempt fails because the target table does not exist, the framework resets the connection, creates the table, and retries the copy -- all transparently within the
run()method.
Usage
Use Database Data Loading when:
- Your pipeline task has produced tabular data that needs to be persisted in a relational database.
- You need high-throughput bulk insertion rather than row-by-row
INSERTstatements. - You want transactional guarantees so that partial loads do not leave the database in an inconsistent state.
- You need pre-copy or post-copy processing (e.g., truncating a rolling window of data, deduplication, or metadata enrichment) to execute atomically with the data load.
- You want the framework to handle table creation automatically when the target table does not yet exist.
Theoretical Basis
Database Data Loading implements the Template Method pattern, where the run() method defines the invariant algorithm skeleton, and subclasses override specific steps (like rows(), copy(), init_copy(), post_copy()) to customize behavior.
The algorithm proceeds as follows:
- Validate configuration: Verify that
tableandcolumnsare specified; raise an exception if not. - Establish connection: Call
self.output().connect()to obtain a database connection from the target. - Generate and stage data: Iterate over
self.rows(), applymap_column()to each value, join values with the column separator, and write each row to a temporary file (for file-based bulk copy) or accumulate into batches (for batch insert). - Begin copy loop (up to 2 attempts):
- Open a cursor on the connection.
- Execute
self.init_copy(connection)for pre-copy operations (e.g., adding metadata columns, truncating old data). - Execute
self.copy(cursor, file)to perform the bulk data transfer. For PostgreSQL, this issues aCOPY ... FROM STDINcommand. For MySQL, this callscursor.executemany()in batches. For SQLAlchemy, this inserts rows in chunks viaconn.execute(ins, rows). - Execute
self.post_copy(connection)for post-copy operations. - If metadata columns are enabled, execute
self.post_copy_metacolumns(cursor).
- Handle table-not-found error: If the first attempt raises a "table does not exist" error, reset the connection, call
self.create_table(connection), and retry from step 4. - Mark completion: Call
self.output().touch(connection)to record successful completion in the marker table (within the same transaction). - Commit and clean up: Commit the transaction, close the connection, and close the temporary file.
This design ensures that the entire load operation is atomic: either all data is loaded and marked complete, or none of it is.