InnoDB stores rows in primary key order (clustered index). This means:
-**Sequential keys = optimal inserts**: new rows append, minimizing page splits and fragmentation.
-**Random keys = fragmentation**: random inserts cause page splits to maintain PK order, wasting space and slowing inserts.
-**Secondary index lookups**: secondary indexes store the PK value and use it to fetch the full row from the clustered index.
## INT vs BIGINT for Primary Keys
-**INT UNSIGNED**: 4 bytes, max ~4.3B rows.
-**BIGINT UNSIGNED**: 8 bytes, max ~18.4 quintillion rows.
Guideline: default to **BIGINT UNSIGNED** unless you're certain the table will never approach the INT limit. The extra 4 bytes is usually cheaper than the risk of exhausting INT.
## Avoid Random UUID as Clustered PK
- UUID PK stored as `BINARY(16)`: 16 bytes (vs 8 for BIGINT). Random inserts cause page splits, and every secondary index entry carries the PK.
- UUID stored as `CHAR(36)`/`VARCHAR(36)`: 36 bytes (+ overhead) and is generally worse for storage and index size.
- If external identifiers are required, store UUID as `BINARY(16)` in a secondary unique column:
```sql
CREATETABLEusers(
idBIGINTUNSIGNEDNOTNULLAUTO_INCREMENTPRIMARYKEY,
public_idBINARY(16)NOTNULL,
UNIQUEKEYidx_public_id(public_id)
);
-- UUID_TO_BIN(uuid, 1) reorders UUIDv1 bytes to be roughly time-sorted (reduces fragmentation)
-- MySQL's UUID() returns UUIDv4 (random). For time-ordered IDs, use app-generated UUIDv7/ULID/Snowflake.
If UUIDs are required, prefer time-ordered variants such as UUIDv7 (app-generated) to reduce index fragmentation.
## Secondary Indexes Include the Primary Key
InnoDB secondary indexes store the primary key value with each index entry. Implications:
-**Larger secondary indexes**: a secondary index entry includes (indexed columns + PK bytes).
-**Covering reads**: `SELECT id FROM users WHERE email = ?` can often be satisfied from `INDEX(email)` because `id` (PK) is already present in the index entry.
-**UUID penalty**: a `BINARY(16)` PK makes every secondary index entry 8 bytes larger than a BIGINT PK.
## Auto-Increment Considerations
-**Hot spot**: inserts target the end of the clustered index (usually fine; can bottleneck at extreme insert rates).
-**Gaps are normal**: rollbacks or failed inserts can leave gaps.
-**Locking**: auto-increment allocation can introduce contention under very high concurrency.
## Alternative Ordered IDs (Snowflake / ULID / UUIDv7)
If you need globally unique IDs generated outside the database:
-**Snowflake-style**: 64-bit integers (fits in BIGINT), time-ordered, compact.
-**ULID / UUIDv7**: 128-bit (store as `BINARY(16)`), time-ordered, better insert locality than random UUIDv4.
Recommendation: prefer `BIGINT AUTO_INCREMENT` unless you need distributed ID generation or externally meaningful identifiers.
## Replication Considerations
- Random-key insert patterns (UUIDv4) can amplify page splits and I/O on replicas too, increasing lag.
- Time-ordered IDs reduce fragmentation and tend to replicate more smoothly under heavy insert workloads.
## Composite Primary Keys
Use for join/many-to-many tables. Most-queried column first:
For infix search at scale, use `FULLTEXT` (when appropriate) or a dedicated search engine.
## `OR` Across Different Columns
`OR` across different columns often prevents efficient index use.
```sql
-- Often suboptimal
WHEREstatus='active'ORregion='us-east'
-- Often better: two indexed queries
SELECT*FROMordersWHEREstatus='active'
UNIONALL
SELECT*FROMordersWHEREregion='us-east';
```
MySQL can sometimes use `index_merge`, but it's frequently slower than a purpose-built composite index or a UNION rewrite.
## ORDER BY + LIMIT Without an Index
`LIMIT` does not automatically make sorting cheap. If no index supports the order, MySQL may sort many rows (`Using filesort`) and then apply LIMIT.
```sql
-- Needs an index on created_at (or it will filesort)
SELECT*FROMordersORDERBYcreated_atDESCLIMIT10;
-- For WHERE + ORDER BY, you usually need a composite index:
-- (status, created_at DESC)
SELECT*FROMorders
WHEREstatus='pending'
ORDERBYcreated_atDESC
LIMIT10;
```
## DISTINCT / GROUP BY
`DISTINCT` and `GROUP BY` can trigger temp tables and sorts (`Using temporary`, `Using filesort`) when indexes don't match.
```sql
-- Often improved by an index on (status)
SELECTDISTINCTstatusFROMorders;
-- Often improved by an index on (status)
SELECTstatus,COUNT(*)FROMordersGROUPBYstatus;
```
## Derived Tables / CTE Materialization
Derived tables and CTEs may be materialized into temporary tables, which can be slower than a flattened query. If performance is surprising, check `EXPLAIN` and consider rewriting the query or adding supporting indexes.
## Other Quick Rules
-**`OFFSET` pagination**: `OFFSET N` scans and discards N rows. Use cursor-based pagination.
-**`SELECT *`** defeats covering indexes. Select only needed columns.
-**`NOT IN` with NULLs**: `NOT IN (subquery)` returns no rows if subquery contains any NULL. Use `NOT EXISTS`.
-**`COUNT(*)` vs `COUNT(col)`**: `COUNT(*)` counts all rows; `COUNT(col)` skips NULLs.
-**Arithmetic on indexed columns**: `WHERE price * 1.1 > 100` prevents index use. Rewrite to keep the column bare: `WHERE price > 100 / 1.1`.
**Warning**: `Seconds_Behind_Source` measures relay-log lag, not true wall-clock staleness. It can underreport during long-running transactions because it only updates when transactions commit.
**GTID-based lag**: for more accurate tracking, compare `@@global.gtid_executed` (replica) to primary GTID position, or use `WAIT_FOR_EXECUTED_GTID_SET()` to wait for a specific transaction.
**Note**: parallel replication with `replica_parallel_type=LOGICAL_CLOCK` requires `binlog_format=ROW`. Statement-based replication (`binlog_format=STATEMENT`) is more limited for parallel apply.
## Mitigation Strategies
| Strategy | How | Trade-off |
|---|---|---|
| **Read from primary** | Route critical reads to primary after writes | Increases primary load |
| **Sticky sessions** | Pin user to primary for N seconds after a write | Adds session affinity complexity |
| **GTID wait** | `SELECT WAIT_FOR_EXECUTED_GTID_SET('gtid', timeout)` on replica | Adds latency equal to lag |
| **Semi-sync replication** | Primary waits for >=1 replica ACK before committing | Higher write latency |
## Common Pitfalls
-**Large transactions cause lag spikes**: A single `INSERT ... SELECT` of 1M rows replays as one big transaction on the replica. Break into batches.
-**DDL blocks replication**: `ALTER TABLE` with `ALGORITHM=COPY` on primary replays on replica, blocking other relay-log events during execution. `INSTANT` and `INPLACE` DDL are less blocking but still require brief metadata locks.
-**Long queries on replica**: A slow `SELECT` on the replica can block relay-log application. Use `replica_parallel_workers` (8.0+) with `replica_parallel_type=LOGICAL_CLOCK` for parallel apply. Note: LOGICAL_CLOCK requires `binlog_format=ROW` and `slave_preserve_commit_order=ON` (or `replica_preserve_commit_order=ON`) to preserve commit order.
-**IO thread bottlenecks**: Network latency, disk I/O, or `relay_log_space_limit` exhaustion can cause lag even when the SQL apply thread isn't saturated. Monitor `Relay_Log_Space` and connectivity.
## Guidelines
- Assume replicas are always slightly behind. Design reads accordingly.
- Use GTID-based replication for reliable failover and lag tracking.
- Monitor `Seconds_Behind_Source` with alerting (>5s warrants investigation).
InnoDB uses row-level locking, but the actual locked range is often wider than expected.
## Next-Key Locks (REPEATABLE READ)
InnoDB's default isolation level uses next-key locks for **locking reads** (`SELECT ... FOR UPDATE`, `SELECT ... FOR SHARE`, `UPDATE`, `DELETE`) to prevent phantom reads. A range scan locks every gap in that range. Plain `SELECT` statements use consistent reads (MVCC) and don't acquire locks.
**Exception**: a unique index search with a unique search condition (e.g., `WHERE id = 5` on a unique `id`) locks only the index record, not the gap. Gap/next-key locks still apply for range scans and non-unique searches.
```sql
-- Locks rows with id 5..10 AND the gaps between them and after the range
SELECT*FROMordersWHEREidBETWEEN5AND10FORUPDATE;
-- Another session inserting id=7 blocks until the lock is released.
```
## Gap Locks on Non-Existent Rows
`SELECT ... FOR UPDATE` on a row that doesn't exist still places a gap lock:
```sql
-- No row with id=999 exists, but this locks the gap around where 999 would be
SELECT*FROMordersWHEREid=999FORUPDATE;
-- Concurrent INSERTs into that gap are blocked.
```
## Index-Less UPDATE/DELETE = Full Scan and Broad Locking
If the WHERE column has no index, InnoDB must scan all rows and locks every row examined (often effectively all rows in the table). This is not table-level locking—InnoDB doesn't escalate locks—but rather row-level locks on all rows:
```sql
-- No index on status → locks all rows (not a table lock, but all row locks)
UPDATEordersSETprocessed=1WHEREstatus='pending';
-- Fix: CREATE INDEX idx_status ON orders (status);
```
## SELECT ... FOR SHARE (Shared Locks)
`SELECT ... FOR SHARE` acquires shared (S) locks instead of exclusive (X) locks. Multiple sessions can hold shared locks simultaneously, but exclusive locks are blocked:
```sql
-- Session 1: shared lock
SELECT*FROMordersWHEREid=5FORSHARE;
-- Session 2: also allowed (shared lock)
SELECT*FROMordersWHEREid=5FORSHARE;
-- Session 3: blocked until shared locks are released
UPDATEordersSETstatus='processed'WHEREid=5;
```
Gap/next-key locks can still apply in REPEATABLE READ, so inserts into locked gaps may be blocked even with shared locks.
## INSERT ... ON DUPLICATE KEY UPDATE
Takes an exclusive next-key lock on the index entry. If multiple sessions do this concurrently on nearby key values, gap-lock deadlocks are common.
## Lock Escalation Misconception
InnoDB does **not** automatically escalate row locks to table locks. When a missing index causes "table-wide" locking, it's because InnoDB scans and locks all rows individually—not because locks were escalated.
## Mitigation Strategies
-**Use READ COMMITTED** when gap locks cause excessive blocking (gap locks disabled in RC except for FK/duplicate-key checks).
-**Keep transactions short** — hold locks for milliseconds, not seconds.
-**Ensure WHERE columns are indexed** to avoid full-table lock scans.
description:Extract text and structure from scans, images, and scanned PDFs. Use for OCR, searchable PDFs, table extraction, receipt parsing, and business card parsing.
---
# OCR Document Processor
Handle OCR-heavy inputs where text must be recovered from images or scanned pages.
## Use This For
- OCR on images and scanned PDFs
- Searchable PDF export
- Structured extraction to text, markdown, JSON, or HTML
- Table extraction from scanned material
- Receipt parsing and business card parsing
## Workflow
1. Decide whether plain OCR, structured extraction, or document-specific parsing is needed.
2. Preprocess noisy inputs before extraction when skew, blur, or shadows are present.
3. Use `scripts/ocr_processor.py` for core OCR tasks.
4. Use the focused helpers when the input is specialized:
-`scripts/business_card_scanner.py`
-`scripts/receipt_scanner.py`
5. Return confidence caveats when the source is low quality, rotated, handwritten, or multilingual.
## Guardrails
- Prefer explicit language selection when accuracy matters.
- Do not claim fields are exact when OCR confidence is weak.
- Route non-scanned digital PDFs to `document-converter-suite` instead of OCR by default.
description:Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
license:Proprietary. LICENSE.txt has complete terms
---
# PDF Processing Guide
## Overview
This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see REFERENCE.md. If you need to fill out a PDF form, read FORMS.md and follow its instructions.
story.append(Paragraph("Content for page 2",styles['Normal']))
# Build PDF
doc.build(story)
```
#### Subscripts and Superscripts
**IMPORTANT**: Never use Unicode subscript/superscript characters (₀₁₂₃₄₅₆₇₈₉, ⁰¹²³⁴⁵⁶⁷⁸⁹) in ReportLab PDFs. The built-in fonts do not include these glyphs, causing them to render as solid black boxes.
Instead, use ReportLab's XML markup tags in Paragraph objects:
**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**
If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory:
`python scripts/check_fillable_fields <file.pdf>`, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
# Fillable fields
If the PDF has fillable form fields:
- Run this script from this file's directory: `python scripts/extract_form_field_info.py <input.pdf> <field_info.json>`. It will create a JSON file with a list of fields in this format:
```
[
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
"type": ("text", "checkbox", "radio_group", or "choice"),
},
// Checkboxes have "checked_value" and "unchecked_value" properties:
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"type": "checkbox",
"checked_value": (Set the field to this value to check the checkbox),
"unchecked_value": (Set the field to this value to uncheck the checkbox),
},
// Radio groups have a "radio_options" list with the possible choices.
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"type": "radio_group",
"radio_options": [
{
"value": (set the field to this value to select this radio option),
"rect": (bounding box for the radio button for this option)
},
// Other radio options
]
},
// Multiple choice fields have a "choice_options" list with the possible choices:
{
"field_id": (unique ID for the field),
"page": (page number, 1-based),
"type": "choice",
"choice_options": [
{
"value": (set the field to this value to select this option),
"text": (display text of the option)
},
// Other choice options
],
}
]
```
- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
- Create a `field_values.json` file in this format with the values to be entered for each field:
```
[
{
"field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
"description": "The user's last name",
"page": 1, // Must match the "page" value in field_info.json
"value": "Simpson"
},
{
"field_id": "Checkbox12",
"description": "Checkbox to be checked if the user is 18 or over",
"page": 1,
"value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
},
// more fields
]
```
- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF:
This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.
# Non-fillable fields
If the PDF doesn't have fillable form fields, you'll add text annotations. First try to extract coordinates from the PDF structure (more accurate), then fall back to visual estimation if needed.
## Step 1: Try Structure Extraction First
Run this script to extract text labels, lines, and checkboxes with their exact PDF coordinates:
-**labels**: Every text element with exact coordinates (x0, top, x1, bottom in PDF points)
-**lines**: Horizontal lines that define row boundaries
-**checkboxes**: Small square rectangles that are checkboxes (with center coordinates)
-**row_boundaries**: Row top/bottom positions calculated from horizontal lines
**Check the results**: If `form_structure.json` has meaningful labels (text elements that correspond to form fields), use **Approach A: Structure-Based Coordinates**. If the PDF is scanned/image-based and has few or no labels, use **Approach B: Visual Estimation**.
Use this when `extract_form_structure.py` found text labels in the PDF.
### A.1: Analyze the Structure
Read form_structure.json and identify:
1.**Label groups**: Adjacent text elements that form a single label (e.g., "Last" + "Name")
2.**Row structure**: Labels with similar `top` values are in the same row
3.**Field columns**: Entry areas start after label ends (x0 = label.x1 + gap)
4.**Checkboxes**: Use the checkbox coordinates directly from the structure
**Coordinate system**: PDF coordinates where y=0 is at TOP of page, y increases downward.
### A.2: Check for Missing Elements
The structure extraction may not detect all form elements. Common cases:
-**Circular checkboxes**: Only square rectangles are detected as checkboxes
-**Complex graphics**: Decorative elements or non-standard form controls
-**Faded or light-colored elements**: May not be extracted
If you see form fields in the PDF images that aren't in form_structure.json, you'll need to use **visual analysis** for those specific fields (see "Hybrid Approach" below).
### A.3: Create fields.json with PDF Coordinates
For each field, calculate entry coordinates from the extracted structure:
**Text fields:**
- entry x0 = label x1 + 5 (small gap after label)
- entry x1 = next label's x0, or row boundary
- entry top = same as label top
- entry bottom = row boundary line below, or label bottom + row_height
**Checkboxes:**
- Use the checkbox rectangle coordinates directly from form_structure.json
This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
---
## Hybrid Approach: Structure + Visual
Use this when structure extraction works for most fields but misses some elements (e.g., circular checkboxes, unusual form controls).
1.**Use Approach A** for fields that were detected in form_structure.json
2.**Convert PDF to images** for visual analysis of missing fields
3.**Use zoom refinement** (from Approach B) for the missing fields
4.**Combine coordinates**: For fields from structure extraction, use `pdf_width`/`pdf_height`. For visually-estimated fields, you must convert image coordinates to PDF coordinates:
- pdf_x = image_x * (pdf_width / image_width)
- pdf_y = image_y * (pdf_height / image_height)
5.**Use a single coordinate system** in fields.json - convert all to PDF coordinates with `pdf_width`/`pdf_height`
---
## Step 2: Validate Before Filling
**Always validate bounding boxes before filling:**
This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions.
## pypdfium2 Library (Apache/BSD License)
### Overview
pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement.
### Render PDF to Images
```python
importpypdfium2aspdfium
fromPILimportImage
# Load PDF
pdf=pdfium.PdfDocument("document.pdf")
# Render page to image
page=pdf[0]# First page
bitmap=page.render(
scale=2.0,# Higher resolution
rotation=0# No rotation
)
# Convert to PIL Image
img=bitmap.to_pil()
img.save("page_1.png","PNG")
# Process multiple pages
fori,pageinenumerate(pdf):
bitmap=page.render(scale=1.5)
img=bitmap.to_pil()
img.save(f"page_{i+1}.jpg","JPEG",quality=90)
```
### Extract Text with pypdfium2
```python
importpypdfium2aspdfium
pdf=pdfium.PdfDocument("document.pdf")
fori,pageinenumerate(pdf):
text=page.get_text()
print(f"Page {i+1} text length: {len(text)} chars")
```
## JavaScript Libraries
### pdf-lib (MIT License)
pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment.
messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})")
else:
messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})")
iflen(messages)>=20:
messages.append("Aborting further checks; fix bounding boxes and try again")
messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.")
iflen(messages)>=20:
messages.append("Aborting further checks; fix bounding boxes and try again")
returnmessages
ifnothas_error:
messages.append("SUCCESS: All bounding boxes are valid")