-
-
Notifications
You must be signed in to change notification settings - Fork 65
✨ add support for SQLite shorthand foreign key references that implicitly reference primary keys #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThe changes introduce two helper methods for SQLite table introspection and enhance foreign key processing to support composite foreign keys. When explicit column references are missing, the system now resolves them by querying the referenced table's primary key columns, enabling correct multi-column FOREIGN KEY clause generation. Changes
Sequence DiagramsequenceDiagram
participant User
participant Transporter
participant SQLite
participant Logger
User->>Transporter: _add_foreign_keys(cursor, mysql_conn)
Transporter->>SQLite: Query foreign keys from table
SQLite-->>Transporter: Foreign key rows
Note over Transporter: Accumulate rows by id<br/>Sort by sequence
alt References explicitly specify columns
Transporter->>Transporter: Use referenced columns
else References omitted (shorthand)
rect rgb(230, 240, 250)
Transporter->>Transporter: _get_table_primary_key_columns()
Transporter->>SQLite: PRAGMA table_info for referenced table
SQLite-->>Transporter: Table schema (PK columns)
Transporter->>Transporter: Resolve referenced columns from PK
end
end
Note over Transporter: Build multi-column<br/>FOREIGN KEY clause
Transporter->>SQLite: Execute ALTER TABLE ADD CONSTRAINT
alt Success
SQLite-->>Transporter: OK
Transporter->>Logger: Log foreign key added
else PK resolution failed
rect rgb(255, 240, 240)
Transporter->>Logger: Warn unresolved referenced PK
Transporter->>Transporter: Skip this FK
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
REFERENCEsREFERENCEs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
tests/unit/sqlite3_to_mysql_test.py (2)
1522-1567: Remove unused test parameter.The
mysql_databaseparameter is created but never used in this test. Consider removing it from the fixture parameters to keep the test focused.def test_add_foreign_keys_shorthand_references_primary_key( self, sqlite_database: str, - mysql_database: Engine, mysql_credentials: MySQLCredentials, mocker: MockFixture, ) -> None:
1568-1614: Remove unused test parameter.The
mysql_databaseparameter is created but never used in this test. Consider removing it from the fixture parameters.def test_add_foreign_keys_shorthand_pk_mismatch_is_skipped( self, sqlite_database: str, - mysql_database: Engine, mysql_credentials: MySQLCredentials, mocker: MockFixture, ) -> None:
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/sqlite3_to_mysql/transporter.py(2 hunks)tests/unit/sqlite3_to_mysql_test.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
tests/{unit,func}/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Place unit tests under tests/unit and functional/CLI tests under tests/func
Files:
tests/unit/sqlite3_to_mysql_test.py
tests/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Format test code with Black/isort and respect Flake8’s 88-column soft cap
Files:
tests/unit/sqlite3_to_mysql_test.py
tests/unit/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Add unit tests for new behavior, isolating pure functions
Files:
tests/unit/sqlite3_to_mysql_test.py
src/sqlite3_to_mysql/transporter.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/sqlite3_to_mysql/transporter.py: Keep SQLite3toMySQL.transfer() flow: create DB (if missing) → create tables → optionally truncate → bulk insert (chunked or streamed) → then create indices → then add foreign keys
On table creation failures due to expression DEFAULTs, retry without DEFAULTs (two-pass _create_table with skip_default=True)
Initialize MySQL capability booleans (e.g., _mysql_fulltext_support, _allow_expr_defaults) early in init, and gate conditional behavior on them
Extend column type translation only in _translate_type_from_sqlite_to_mysql instead of scattering conversions
Handle DEFAULT normalization in _translate_default_for_mysql; return empty string to suppress invalid defaults
During index creation, handle duplicate names by appending numeric suffixes, unless _ignore_duplicate_keys is set to skip retries
Implement chunked transfer with cursor.fetchmany when --chunk is set; otherwise use fetchall with tqdm progress
Only add foreign keys when no table include/exclude filters are set and --without-foreign-keys is not in effect
Implement insert methods: IGNORE (default), UPDATE using ON DUPLICATE KEY UPDATE (with optional VALUES alias), and DEFAULT (no modifiers)
Expose new capability booleans from init of SQLite3toMySQL for downstream logic
Use prepared cursors (cursor(prepared=True)) and batch inserts via executemany for performance
For large transfers, prefer --chunk and preserve commit granularity in any new bulk path
Files:
src/sqlite3_to_mysql/transporter.py
src/sqlite3_to_mysql/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/sqlite3_to_mysql/**/*.py: Always wrap dynamic table/index/column names with safe_identifier_length(...) before emitting SQL
Use the class _logger for logging; do not print directly; respect the quiet flag for progress/INFO while always emitting errors
Files:
src/sqlite3_to_mysql/transporter.py
src/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/**/*.py: Format with Black (line length 120) and isort (profile=black); adhere to Flake8’s 88-column soft cap to avoid long chained expressions on one line
Preserve and add type hints for new public interfaces
Files:
src/sqlite3_to_mysql/transporter.py
src/sqlite3_to_mysql/{cli.py,transporter.py}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
In debug mode (--debug), surface exceptions instead of swallowing them
Files:
src/sqlite3_to_mysql/transporter.py
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/{mysql_utils.py,sqlite_utils.py} : Add new MySQL/SQLite capability checks in mysql_utils.py/sqlite_utils.py and keep them as dedicated helpers
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Only add foreign keys when no table include/exclude filters are set and --without-foreign-keys is not in effect
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Extend column type translation only in _translate_type_from_sqlite_to_mysql instead of scattering conversions
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Keep SQLite3toMySQL.transfer() flow: create DB (if missing) → create tables → optionally truncate → bulk insert (chunked or streamed) → then create indices → then add foreign keys
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/{mysql_utils.py,sqlite_utils.py} : Centralize dialect/version feature checks, type adaptation, and identifier safety in mysql_utils.py and sqlite_utils.py; add new MySQL capability gates here
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/**/*.py : Always wrap dynamic table/index/column names with safe_identifier_length(...) before emitting SQL
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/cli.py : Make --sqlite-tables and --exclude-sqlite-tables mutually exclusive; setting either implies --without-foreign-keys
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/{mysql_utils.py,sqlite_utils.py} : Add new MySQL/SQLite capability checks in mysql_utils.py/sqlite_utils.py and keep them as dedicated helpers
Applied to files:
tests/unit/sqlite3_to_mysql_test.pysrc/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Only add foreign keys when no table include/exclude filters are set and --without-foreign-keys is not in effect
Applied to files:
tests/unit/sqlite3_to_mysql_test.pysrc/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/cli.py : Make --sqlite-tables and --exclude-sqlite-tables mutually exclusive; setting either implies --without-foreign-keys
Applied to files:
tests/unit/sqlite3_to_mysql_test.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Keep SQLite3toMySQL.transfer() flow: create DB (if missing) → create tables → optionally truncate → bulk insert (chunked or streamed) → then create indices → then add foreign keys
Applied to files:
tests/unit/sqlite3_to_mysql_test.pysrc/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/{mysql_utils.py,sqlite_utils.py} : Centralize dialect/version feature checks, type adaptation, and identifier safety in mysql_utils.py and sqlite_utils.py; add new MySQL capability gates here
Applied to files:
tests/unit/sqlite3_to_mysql_test.pysrc/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Extend column type translation only in _translate_type_from_sqlite_to_mysql instead of scattering conversions
Applied to files:
tests/unit/sqlite3_to_mysql_test.pysrc/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/**/*.py : Always wrap dynamic table/index/column names with safe_identifier_length(...) before emitting SQL
Applied to files:
tests/unit/sqlite3_to_mysql_test.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : During index creation, handle duplicate names by appending numeric suffixes, unless _ignore_duplicate_keys is set to skip retries
Applied to files:
tests/unit/sqlite3_to_mysql_test.pysrc/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/**/*.py : Use the class _logger for logging; do not print directly; respect the quiet flag for progress/INFO while always emitting errors
Applied to files:
tests/unit/sqlite3_to_mysql_test.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Expose new capability booleans from __init__ of SQLite3toMySQL for downstream logic
Applied to files:
src/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Implement insert methods: IGNORE (default), UPDATE using ON DUPLICATE KEY UPDATE (with optional VALUES alias), and DEFAULT (no modifiers)
Applied to files:
src/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Initialize MySQL capability booleans (e.g., _mysql_fulltext_support, _allow_expr_defaults) early in __init__, and gate conditional behavior on them
Applied to files:
src/sqlite3_to_mysql/transporter.py
📚 Learning: 2025-10-23T18:16:08.709Z
Learnt from: CR
Repo: techouse/sqlite3-to-mysql PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-10-23T18:16:08.709Z
Learning: Applies to src/sqlite3_to_mysql/transporter.py : Use prepared cursors (cursor(prepared=True)) and batch inserts via executemany for performance
Applied to files:
src/sqlite3_to_mysql/transporter.py
🧬 Code graph analysis (2)
tests/unit/sqlite3_to_mysql_test.py (2)
tests/conftest.py (1)
MySQLCredentials(195-202)src/sqlite3_to_mysql/transporter.py (1)
_add_foreign_keys(1177-1256)
src/sqlite3_to_mysql/transporter.py (1)
src/sqlite3_to_mysql/mysql_utils.py (1)
safe_identifier_length(166-168)
🪛 Ruff (0.14.4)
tests/unit/sqlite3_to_mysql_test.py
1525-1525: Unused method argument: mysql_database
(ARG002)
1571-1571: Unused method argument: mysql_database
(ARG002)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: Test (python3.10, mariadb:10.0, 1, true, 3.10)
- GitHub Check: Test (python3.14, mariadb:5.5, 1, true, 3.14)
- GitHub Check: Test (python3.9, mariadb:10.0, 1, true, 3.9)
- GitHub Check: Test (python3.13, mariadb:5.5, 1, true, 3.13)
- GitHub Check: Test (python3.10, mariadb:5.5, 1, true, 3.10)
- GitHub Check: Test (python3.11, mariadb:5.5, 1, true, 3.11)
- GitHub Check: Test (python3.12, mariadb:5.5, 1, true, 3.12)
- GitHub Check: Test (python3.9, mariadb:5.5, 1, true, 3.9)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (3)
src/sqlite3_to_mysql/transporter.py (3)
300-306: LGTM!The helper method correctly fetches SQLite table metadata using the appropriate pragma and properly quotes identifiers.
307-318: LGTM!The method correctly extracts and orders primary key columns whilst properly excluding hidden columns and applying safe identifier length constraints.
1177-1256: This review comment is incorrect.The authoritative coding guidelines state: "Foreign keys only added when neither table include/exclude filters nor
--without-foreign-keyseffective". There is no requirement in the PR objectives to restrict foreign keys based on column types (TEXT or BLOB).TEXT/BLOB handling exists only for index creation (lines 1001–1028), where it prevents invalid index types. Foreign keys do not have this constraint in MySQL—they can reference TEXT or BLOB columns without length specifications, unlike indices.
The implementation correctly follows the documented requirements: it adds foreign keys when appropriate filters and flags permit, which is the intended behaviour.
Likely an incorrect or invalid review comment.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #153 +/- ##
==========================================
- Coverage 98.41% 98.27% -0.15%
==========================================
Files 8 8
Lines 1075 1100 +25
==========================================
+ Hits 1058 1081 +23
- Misses 17 19 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
REFERENCEs
This pull request significantly improves the handling of foreign key constraints during SQLite to MySQL migration, especially for shorthand foreign key references that implicitly reference primary keys. The changes add robust logic to resolve referenced columns, handle edge cases where references are ambiguous or mismatched, and introduce targeted unit tests to verify these scenarios.
Enhancements to foreign key handling:
_get_table_infoand_get_table_primary_key_columnshelper methods intransporter.pyto fetch table metadata and resolve primary key columns for referenced tables._add_foreign_keysto support shorthand foreign key definitions by resolving missing referenced columns using the referenced table's primary key(s). Added logic to skip and warn on ambiguous or mismatched references, and updated SQL generation to support composite keys.Testing improvements:
test_add_foreign_keys_shorthand_references_primary_keyverifies that foreign keys with shorthand references are correctly resolved to the referenced table's primary key.test_add_foreign_keys_shorthand_pk_mismatch_is_skippedensures that foreign keys are skipped and a warning is logged if the column count does not match the referenced table's primary key.Partially fixes #75
Supersedes #78