Skip to content

Commit 4aeb79d

Browse files
authored
Fix index error when ALTER table (#147)
1 parent b68d53e commit 4aeb79d

File tree

3 files changed

+135
-4
lines changed

3 files changed

+135
-4
lines changed

mysql_ch_replicator/converter.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -530,11 +530,20 @@ def get_db_and_table_name(self, token, db_name):
530530
table_name = token
531531
db_name = strip_sql_name(db_name)
532532
table_name = strip_sql_name(table_name)
533+
533534
if self.db_replicator:
534-
# Check if database and table match config BEFORE applying mapping
535-
matches_config = (
536-
self.db_replicator.config.is_database_matches(db_name)
537-
and self.db_replicator.config.is_table_matches(table_name))
535+
# If we're dealing with a relative table name (no DB prefix), we need to check
536+
# if the current db_name is already a target database name
537+
if '.' not in token and self.db_replicator.target_database == db_name:
538+
# This is a target database name, so for config matching we need to use the source database
539+
matches_config = (
540+
self.db_replicator.config.is_database_matches(self.db_replicator.database)
541+
and self.db_replicator.config.is_table_matches(table_name))
542+
else:
543+
# Normal case: check if source database and table match config
544+
matches_config = (
545+
self.db_replicator.config.is_database_matches(db_name)
546+
and self.db_replicator.config.is_table_matches(table_name))
538547

539548
# Apply database mapping AFTER checking matches_config
540549
if db_name == self.db_replicator.database:

test_mysql_ch_replicator.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import json
77
import uuid
88
import decimal
9+
import tempfile
10+
import yaml
911

1012
import pytest
1113
import requests
@@ -2278,6 +2280,9 @@ def test_schema_evolution_with_db_mapping():
22782280
clickhouse_settings=cfg.clickhouse,
22792281
)
22802282

2283+
ch.drop_database("mapped_target_db")
2284+
assert_wait(lambda: "mapped_target_db" not in ch.get_databases())
2285+
22812286
prepare_env(cfg, mysql, ch, db_name=TEST_DB_NAME)
22822287

22832288
# Create a test table with some columns using fully qualified name
@@ -2352,3 +2357,100 @@ def test_schema_evolution_with_db_mapping():
23522357
# Clean up
23532358
db_replicator_runner.stop()
23542359
binlog_replicator_runner.stop()
2360+
2361+
2362+
def test_dynamic_column_addition_user_config():
2363+
"""Test to verify handling of dynamically added columns using user's exact configuration.
2364+
2365+
This test reproduces the issue where columns are added on-the-fly via UPDATE
2366+
rather than through ALTER TABLE statements, leading to an index error in the converter.
2367+
"""
2368+
config_path = 'tests_config_dynamic_column.yaml'
2369+
2370+
cfg = config.Settings()
2371+
cfg.load(config_path)
2372+
2373+
mysql = mysql_api.MySQLApi(
2374+
database=None,
2375+
mysql_settings=cfg.mysql,
2376+
)
2377+
2378+
ch = clickhouse_api.ClickhouseApi(
2379+
database=None,
2380+
clickhouse_settings=cfg.clickhouse,
2381+
)
2382+
2383+
prepare_env(cfg, mysql, ch, db_name='test_replication')
2384+
2385+
# Prepare environment - drop and recreate databases
2386+
mysql.drop_database("test_replication")
2387+
mysql.create_database("test_replication")
2388+
mysql.set_database("test_replication")
2389+
ch.drop_database("test_replication_ch")
2390+
assert_wait(lambda: "test_replication_ch" not in ch.get_databases())
2391+
2392+
# Create the exact table structure from the user's example
2393+
mysql.execute('''
2394+
CREATE TABLE test_replication.replication_data (
2395+
code VARCHAR(255) NOT NULL PRIMARY KEY,
2396+
val_1 VARCHAR(255) NOT NULL
2397+
);
2398+
''')
2399+
2400+
# Insert initial data
2401+
mysql.execute(
2402+
"INSERT INTO test_replication.replication_data(code, val_1) VALUE ('test-1', '1');",
2403+
commit=True,
2404+
)
2405+
2406+
# Start the replication processes
2407+
binlog_replicator_runner = BinlogReplicatorRunner(cfg_file=config_path)
2408+
binlog_replicator_runner.run()
2409+
db_replicator_runner = DbReplicatorRunner("test_replication", cfg_file=config_path)
2410+
db_replicator_runner.run()
2411+
2412+
# Wait for initial replication to complete
2413+
assert_wait(lambda: "test_replication_ch" in ch.get_databases())
2414+
2415+
# Set the database before checking tables
2416+
ch.execute_command("USE test_replication_ch")
2417+
assert_wait(lambda: "replication_data" in ch.get_tables())
2418+
assert_wait(lambda: len(ch.select("replication_data")) == 1)
2419+
2420+
# Verify initial data was replicated correctly
2421+
assert_wait(lambda: ch.select("replication_data", where="code='test-1'")[0]['val_1'] == '1')
2422+
2423+
# Update an existing field - this should work fine
2424+
mysql.execute("UPDATE test_replication.replication_data SET val_1 = '1200' WHERE code = 'test-1';", commit=True)
2425+
assert_wait(lambda: ch.select("replication_data", where="code='test-1'")[0]['val_1'] == '1200')
2426+
2427+
mysql.execute("USE test_replication");
2428+
2429+
# Add val_2 column
2430+
mysql.execute("ALTER TABLE replication_data ADD COLUMN val_2 VARCHAR(255);", commit=True)
2431+
2432+
# Now try to update with a field that doesn't exist
2433+
# This would have caused an error before our fix
2434+
mysql.execute("UPDATE test_replication.replication_data SET val_2 = '100' WHERE code = 'test-1';", commit=True)
2435+
2436+
# Verify replication processes are still running
2437+
binlog_pid = get_binlog_replicator_pid(cfg)
2438+
db_pid = get_db_replicator_pid(cfg, "test_replication")
2439+
2440+
assert binlog_pid is not None, "Binlog replicator process died"
2441+
assert db_pid is not None, "DB replicator process died"
2442+
2443+
# Verify the replication is still working after the dynamic column update
2444+
mysql.execute("UPDATE test_replication.replication_data SET val_1 = '1500' WHERE code = 'test-1';", commit=True)
2445+
assert_wait(lambda: ch.select("replication_data", where="code='test-1'")[0]['val_1'] == '1500')
2446+
2447+
print("Test passed - dynamic column was skipped without breaking replication")
2448+
2449+
# Cleanup
2450+
binlog_pid = get_binlog_replicator_pid(cfg)
2451+
if binlog_pid:
2452+
kill_process(binlog_pid)
2453+
2454+
db_pid = get_db_replicator_pid(cfg, "test_replication")
2455+
if db_pid:
2456+
kill_process(db_pid)

tests_config_dynamic_column.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
mysql:
2+
host: 'localhost'
3+
port: 9306
4+
user: 'root'
5+
password: 'admin'
6+
7+
clickhouse:
8+
host: 'localhost'
9+
port: 9123
10+
user: 'default'
11+
password: 'admin'
12+
13+
binlog_replicator:
14+
data_dir: '/app/binlog/'
15+
records_per_file: 100000
16+
17+
databases: 'test_replication'
18+
19+
target_databases:
20+
test_replication: test_replication_ch

0 commit comments

Comments
 (0)