mysql_cdc.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. import re
  10. from random import Random
  11. from textwrap import dedent
  12. from typing import Any
  13. from materialize.checks.actions import Testdrive
  14. from materialize.checks.checks import Check, externally_idempotent
  15. from materialize.checks.executors import Executor
  16. from materialize.checks.features import Features
  17. from materialize.mz_version import MzVersion
  18. from materialize.mzcompose.services.mysql import MySql
  19. class MySqlCdcBase:
  20. base_version: MzVersion
  21. current_version: MzVersion
  22. wait: bool
  23. suffix: str
  24. repeats: int
  25. expects: int
  26. def __init__(self, wait: bool, **kwargs: Any) -> None:
  27. self.wait = wait
  28. self.repeats = 1024 if wait else 16384
  29. self.expects = 97350 if wait else 1633350
  30. self.suffix = f"_{str(wait).lower()}"
  31. super().__init__(**kwargs) # forward unused args to Check
  32. def initialize(self) -> Testdrive:
  33. return Testdrive(
  34. dedent(
  35. f"""
  36. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  37. $ mysql-execute name=mysql
  38. # create the database if it does not exist yet but do not drop it
  39. CREATE DATABASE IF NOT EXISTS public;
  40. USE public;
  41. CREATE USER mysql1{self.suffix} IDENTIFIED BY 'mysql';
  42. GRANT REPLICATION SLAVE ON *.* TO mysql1{self.suffix};
  43. GRANT ALL ON public.* TO mysql1{self.suffix};
  44. DROP TABLE IF EXISTS mysql_source_table{self.suffix};
  45. # uniqueness constraint not possible for length of 1024 characters upwards (max key length is 3072 bytes)
  46. CREATE TABLE mysql_source_table{self.suffix} (f1 VARCHAR(32), f2 INTEGER, f3 TEXT NOT NULL, f4 JSON, PRIMARY KEY(f1, f2));
  47. SET @i:=0;
  48. CREATE TABLE sequence{self.suffix} (i INT);
  49. INSERT INTO sequence{self.suffix} SELECT (@i:=@i+1) FROM mysql.time_zone t1, mysql.time_zone t2 LIMIT 100;
  50. INSERT INTO mysql_source_table{self.suffix} SELECT 'A', i, REPEAT('A', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  51. > CREATE SECRET mysqlpass1{self.suffix} AS 'mysql';
  52. > CREATE CONNECTION mysql1{self.suffix} TO MYSQL (
  53. HOST 'mysql',
  54. USER mysql1{self.suffix},
  55. PASSWORD SECRET mysqlpass1{self.suffix}
  56. )
  57. """
  58. )
  59. )
  60. def manipulate(self) -> list[Testdrive]:
  61. return [
  62. Testdrive(dedent(s))
  63. for s in [
  64. f"""
  65. > CREATE SOURCE mysql_source1{self.suffix}
  66. FROM MYSQL CONNECTION mysql1{self.suffix};
  67. > CREATE TABLE mysql_source_tableA{self.suffix} FROM SOURCE mysql_source1{self.suffix} (REFERENCE public.mysql_source_table{self.suffix});
  68. > CREATE DEFAULT INDEX ON mysql_source_tableA{self.suffix};
  69. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  70. $ mysql-execute name=mysql
  71. USE public;
  72. SET @i:=0;
  73. INSERT INTO mysql_source_table{self.suffix} SELECT 'B', i, REPEAT('B', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  74. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  75. > CREATE SECRET mysqlpass2{self.suffix} AS 'mysql';
  76. > CREATE CONNECTION mysql2{self.suffix} TO MYSQL (
  77. HOST 'mysql',
  78. USER mysql1{self.suffix},
  79. PASSWORD SECRET mysqlpass2{self.suffix}
  80. )
  81. $ mysql-execute name=mysql
  82. SET @i:=0;
  83. INSERT INTO mysql_source_table{self.suffix} SELECT 'C', i, REPEAT('C', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  84. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  85. """
  86. + (
  87. f"""
  88. # Wait until MySQL snapshot is complete
  89. > SELECT COUNT(*) > 0 FROM mysql_source_tableA{self.suffix}
  90. true
  91. """
  92. if self.wait
  93. else ""
  94. ),
  95. f"""
  96. $ postgres-execute connection=postgres://mz_system@${{testdrive.materialize-internal-sql-addr}}
  97. GRANT USAGE ON CONNECTION mysql2{self.suffix} TO materialize
  98. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  99. $ mysql-execute name=mysql
  100. USE public;
  101. SET @i:=0;
  102. INSERT INTO mysql_source_table{self.suffix} SELECT 'D', i, REPEAT('D', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  103. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  104. > CREATE SOURCE mysql_source2{self.suffix}
  105. FROM MYSQL CONNECTION mysql2{self.suffix};
  106. > CREATE TABLE mysql_source_tableB{self.suffix} FROM SOURCE mysql_source2{self.suffix} (REFERENCE public.mysql_source_table{self.suffix});
  107. $ mysql-execute name=mysql
  108. SET @i:=0;
  109. INSERT INTO mysql_source_table{self.suffix} SELECT 'E', i, REPEAT('E', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  110. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  111. $ mysql-execute name=mysql
  112. SET @i:=0;
  113. INSERT INTO mysql_source_table{self.suffix} SELECT 'F', i, REPEAT('F', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  114. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  115. > CREATE SECRET mysqlpass3{self.suffix} AS 'mysql';
  116. > CREATE CONNECTION mysql3{self.suffix} TO MYSQL (
  117. HOST 'mysql',
  118. USER mysql1{self.suffix},
  119. PASSWORD SECRET mysqlpass3{self.suffix}
  120. )
  121. > CREATE SOURCE mysql_source3{self.suffix}
  122. FROM MYSQL CONNECTION mysql3{self.suffix};
  123. > CREATE TABLE mysql_source_tableC{self.suffix} FROM SOURCE mysql_source3{self.suffix} (REFERENCE public.mysql_source_table{self.suffix});
  124. $ mysql-execute name=mysql
  125. SET @i:=0;
  126. INSERT INTO mysql_source_table{self.suffix} SELECT 'G', i, REPEAT('G', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  127. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  128. $ mysql-execute name=mysql
  129. SET @i:=0;
  130. INSERT INTO mysql_source_table{self.suffix} SELECT 'H', i, REPEAT('X', {self.repeats} - i), NULL FROM sequence{self.suffix} WHERE i <= 100;
  131. UPDATE mysql_source_table{self.suffix} SET f2 = f2 + 100;
  132. """
  133. + (
  134. f"""
  135. # Wait until MySQL snapshot is complete
  136. > SELECT COUNT(*) > 0 FROM mysql_source_tableB{self.suffix}
  137. true
  138. > SELECT COUNT(*) > 0 FROM mysql_source_tableC{self.suffix}
  139. true
  140. """
  141. if self.wait
  142. else ""
  143. ),
  144. ]
  145. ]
  146. def validate(self) -> Testdrive:
  147. sql = dedent(
  148. f"""
  149. $ postgres-execute connection=postgres://mz_system@${{testdrive.materialize-internal-sql-addr}}
  150. GRANT SELECT ON mysql_source_tableA{self.suffix} TO materialize
  151. GRANT SELECT ON mysql_source_tableB{self.suffix} TO materialize
  152. GRANT SELECT ON mysql_source_tableC{self.suffix} TO materialize
  153. > SELECT f1, max(f2), SUM(LENGTH(f3)) FROM mysql_source_tableA{self.suffix} GROUP BY f1;
  154. A 800 {self.expects}
  155. B 800 {self.expects}
  156. C 700 {self.expects}
  157. D 600 {self.expects}
  158. E 500 {self.expects}
  159. F 400 {self.expects}
  160. G 300 {self.expects}
  161. H 200 {self.expects}
  162. > SELECT f1, max(f2), SUM(LENGTH(f3)) FROM mysql_source_tableB{self.suffix} GROUP BY f1;
  163. A 800 {self.expects}
  164. B 800 {self.expects}
  165. C 700 {self.expects}
  166. D 600 {self.expects}
  167. E 500 {self.expects}
  168. F 400 {self.expects}
  169. G 300 {self.expects}
  170. H 200 {self.expects}
  171. > SELECT f1, max(f2), SUM(LENGTH(f3)) FROM mysql_source_tableC{self.suffix} GROUP BY f1;
  172. A 800 {self.expects}
  173. B 800 {self.expects}
  174. C 700 {self.expects}
  175. D 600 {self.expects}
  176. E 500 {self.expects}
  177. F 400 {self.expects}
  178. G 300 {self.expects}
  179. H 200 {self.expects}
  180. # TODO: Figure out the quoting here -- it returns "f4" when done using the SQL shell
  181. # (Might have changed again with https://github.com/MaterializeInc/materialize/pull/31933)
  182. # > SELECT regexp_match(create_sql, 'TEXT COLUMNS = \\((.*?)\\)')[1] FROM (SHOW CREATE SOURCE mysql_source_tableA{self.suffix});
  183. # "\"f4\""
  184. # Confirm that the primary key information has been propagated from MySQL
  185. > SELECT key FROM (SHOW INDEXES ON mysql_source_tableA{self.suffix});
  186. {{f1,f2}}
  187. ?[version>=13500] EXPLAIN OPTIMIZED PLAN AS VERBOSE TEXT FOR SELECT DISTINCT f1, f2 FROM mysql_source_tableA{self.suffix};
  188. Explained Query (fast path):
  189. Project (#0, #1)
  190. ReadIndex on=materialize.public.mysql_source_tablea{self.suffix} mysql_source_tablea{self.suffix}_primary_idx=[*** full scan ***]
  191. Used Indexes:
  192. - materialize.public.mysql_source_tablea{self.suffix}_primary_idx (*** full scan ***)
  193. Target cluster: quickstart
  194. ?[version<13500] EXPLAIN OPTIMIZED PLAN FOR SELECT DISTINCT f1, f2 FROM mysql_source_tableA{self.suffix};
  195. Explained Query (fast path):
  196. Project (#0, #1)
  197. ReadIndex on=materialize.public.mysql_source_tablea{self.suffix} mysql_source_tablea{self.suffix}_primary_idx=[*** full scan ***]
  198. Used Indexes:
  199. - materialize.public.mysql_source_tablea{self.suffix}_primary_idx (*** full scan ***)
  200. Target cluster: quickstart
  201. """
  202. )
  203. return Testdrive(sql)
  204. @externally_idempotent(False)
  205. class MySqlCdc(MySqlCdcBase, Check):
  206. def __init__(
  207. self, base_version: MzVersion, rng: Random | None, features: Features | None
  208. ) -> None:
  209. super().__init__(
  210. wait=True, base_version=base_version, rng=rng, features=features
  211. )
  212. @externally_idempotent(False)
  213. class MySqlCdcNoWait(MySqlCdcBase, Check):
  214. def __init__(
  215. self, base_version: MzVersion, rng: Random | None, features: Features | None
  216. ) -> None:
  217. super().__init__(
  218. wait=False, base_version=base_version, rng=rng, features=features
  219. )
  220. @externally_idempotent(False)
  221. class MySqlCdcMzNow(Check):
  222. def initialize(self) -> Testdrive:
  223. return Testdrive(
  224. dedent(
  225. f"""
  226. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  227. $ mysql-execute name=mysql
  228. # create the database if it does not exist yet but do not drop it
  229. CREATE DATABASE IF NOT EXISTS public;
  230. USE public;
  231. CREATE USER mysql2 IDENTIFIED BY 'mysql';
  232. GRANT REPLICATION SLAVE ON *.* TO mysql2;
  233. GRANT ALL ON public.* TO mysql2;
  234. DROP TABLE IF EXISTS mysql_mz_now_table;
  235. CREATE TABLE mysql_mz_now_table (f1 TIMESTAMP, f2 CHAR(5), PRIMARY KEY (f1, f2));
  236. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'A1');
  237. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'B1');
  238. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'C1');
  239. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'D1');
  240. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'E1');
  241. > CREATE SECRET mysql_mz_now_pass AS 'mysql';
  242. > CREATE CONNECTION mysql_mz_now_conn TO MYSQL (
  243. HOST 'mysql',
  244. USER mysql2,
  245. PASSWORD SECRET mysql_mz_now_pass
  246. )
  247. > CREATE SOURCE mysql_mz_now_source
  248. FROM MYSQL CONNECTION mysql_mz_now_conn;
  249. > CREATE TABLE mysql_mz_now_table FROM SOURCE mysql_mz_now_source (REFERENCE public.mysql_mz_now_table);
  250. # Return all rows fresher than 60 seconds
  251. > CREATE MATERIALIZED VIEW mysql_mz_now_view AS
  252. SELECT * FROM mysql_mz_now_table
  253. WHERE mz_now() <= ROUND(EXTRACT(epoch FROM f1 + INTERVAL '60' SECOND) * 1000)
  254. """
  255. )
  256. )
  257. def manipulate(self) -> list[Testdrive]:
  258. return [
  259. Testdrive(dedent(s))
  260. for s in [
  261. f"""
  262. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  263. $ mysql-execute name=mysql
  264. USE public;
  265. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'A2');
  266. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'B2');
  267. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'C2');
  268. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'D2');
  269. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'E2');
  270. DELETE FROM mysql_mz_now_table WHERE f2 = 'B1';
  271. UPDATE mysql_mz_now_table SET f1 = NOW() WHERE f2 = 'C1';
  272. """,
  273. f"""
  274. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  275. $ mysql-execute name=mysql
  276. USE public;
  277. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'A3');
  278. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'B3');
  279. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'C3');
  280. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'D3');
  281. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'E3');
  282. DELETE FROM mysql_mz_now_table WHERE f2 = 'B2';
  283. UPDATE mysql_mz_now_table SET f1 = NOW() WHERE f2 = 'D1';
  284. """,
  285. ]
  286. ]
  287. def validate(self) -> Testdrive:
  288. return Testdrive(
  289. dedent(
  290. f"""
  291. > SELECT COUNT(*) FROM mysql_mz_now_table;
  292. 13
  293. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  294. $ mysql-execute name=mysql
  295. USE public;
  296. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'A4');
  297. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'B4');
  298. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'C4');
  299. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'D4');
  300. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'E4');
  301. DELETE FROM mysql_mz_now_table WHERE f2 = 'B3';
  302. UPDATE mysql_mz_now_table SET f1 = NOW() WHERE f2 = 'E1'
  303. # Expect some rows newer than 60 seconds in view
  304. > SELECT COUNT(*) >= 6 FROM mysql_mz_now_view
  305. WHERE f1 > NOW() - INTERVAL '60' SECOND;
  306. true
  307. # Expect no rows older than 60 seconds in view
  308. > SELECT COUNT(*) FROM mysql_mz_now_view
  309. WHERE f1 < NOW() - INTERVAL '60' SECOND;
  310. 0
  311. # Rollback the last INSERTs so that validate() can be called multiple times
  312. $ mysql-execute name=mysql
  313. INSERT INTO mysql_mz_now_table VALUES (NOW(), 'B3');
  314. DELETE FROM mysql_mz_now_table WHERE f2 LIKE '%4%';
  315. """
  316. )
  317. )
  318. @externally_idempotent(False)
  319. class MySqlBitType(Check):
  320. def _can_run(self, e: Executor) -> bool:
  321. return self.base_version > MzVersion.parse_mz("v0.131.0-dev")
  322. def initialize(self) -> Testdrive:
  323. return Testdrive(
  324. dedent(
  325. f"""
  326. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  327. $ mysql-execute name=mysql
  328. # create the database if it does not exist yet but do not drop it
  329. CREATE DATABASE IF NOT EXISTS public;
  330. USE public;
  331. CREATE USER mysql3 IDENTIFIED BY 'mysql';
  332. GRANT REPLICATION SLAVE ON *.* TO mysql3;
  333. GRANT ALL ON public.* TO mysql3;
  334. DROP TABLE IF EXISTS mysql_bit_table;
  335. CREATE TABLE mysql_bit_table (f1 BIT(11), f2 BIT(1));
  336. INSERT INTO mysql_bit_table VALUES (8, 0);
  337. INSERT INTO mysql_bit_table VALUES (13, 1)
  338. INSERT INTO mysql_bit_table VALUES (b'11100000100', b'1');
  339. INSERT INTO mysql_bit_table VALUES (b'0000', b'0');
  340. INSERT INTO mysql_bit_table VALUES (b'11111111111', b'0');
  341. > CREATE SECRET mysql_bit_pass AS 'mysql';
  342. > CREATE CONNECTION mysql_bit_conn TO MYSQL (
  343. HOST 'mysql',
  344. USER mysql3,
  345. PASSWORD SECRET mysql_bit_pass
  346. )
  347. > CREATE SOURCE mysql_bit_source
  348. FROM MYSQL CONNECTION mysql_bit_conn;
  349. > CREATE TABLE mysql_bit_table FROM SOURCE mysql_bit_source (REFERENCE public.mysql_bit_table);
  350. # Return all rows
  351. > CREATE MATERIALIZED VIEW mysql_bit_view AS
  352. SELECT * FROM mysql_bit_table
  353. """
  354. )
  355. )
  356. def manipulate(self) -> list[Testdrive]:
  357. return [
  358. Testdrive(dedent(s))
  359. for s in [
  360. f"""
  361. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  362. $ mysql-execute name=mysql
  363. USE public;
  364. INSERT INTO mysql_bit_table VALUES (20, 1);
  365. """,
  366. f"""
  367. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  368. $ mysql-execute name=mysql
  369. USE public;
  370. INSERT INTO mysql_bit_table VALUES (30, 1);
  371. """,
  372. ]
  373. ]
  374. def validate(self) -> Testdrive:
  375. return Testdrive(
  376. dedent(
  377. f"""
  378. > SELECT * FROM mysql_bit_table;
  379. 0 0
  380. 8 0
  381. 13 1
  382. 20 1
  383. 30 1
  384. 1796 1
  385. 2047 0
  386. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  387. $ mysql-execute name=mysql
  388. USE public;
  389. INSERT INTO mysql_bit_table VALUES (40, 1);
  390. > SELECT * FROM mysql_bit_table;
  391. 0 0
  392. 8 0
  393. 13 1
  394. 20 1
  395. 30 1
  396. 40 1
  397. 1796 1
  398. 2047 0
  399. # Rollback the last INSERTs so that validate() can be called multiple times
  400. $ mysql-execute name=mysql
  401. DELETE FROM mysql_bit_table WHERE f1 = 40;
  402. > SELECT * FROM mysql_bit_table;
  403. 0 0
  404. 8 0
  405. 13 1
  406. 20 1
  407. 30 1
  408. 1796 1
  409. 2047 0
  410. """
  411. )
  412. )
  413. @externally_idempotent(False)
  414. class MySqlInvisibleColumn(Check):
  415. def _can_run(self, e: Executor) -> bool:
  416. return self.base_version > MzVersion.parse_mz("v0.133.0-dev")
  417. def initialize(self) -> Testdrive:
  418. return Testdrive(
  419. dedent(
  420. f"""
  421. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  422. $ mysql-execute name=mysql
  423. # create the database if it does not exist yet but do not drop it
  424. CREATE DATABASE IF NOT EXISTS public;
  425. USE public;
  426. CREATE USER mysql4 IDENTIFIED BY 'mysql';
  427. GRANT REPLICATION SLAVE ON *.* TO mysql4;
  428. GRANT ALL ON public.* TO mysql4;
  429. DROP TABLE IF EXISTS mysql_invisible_table;
  430. CREATE TABLE mysql_invisible_table (f1 INT, f2 FLOAT INVISIBLE, f3 DATE INVISIBLE, f4 TEXT INVISIBLE);
  431. INSERT INTO mysql_invisible_table (f1, f2, f3, f4) VALUES (1, 0.1, '2025-01-01', 'one');
  432. > CREATE SECRET mysql_invisible_pass AS 'mysql';
  433. > CREATE CONNECTION mysql_invisible_conn TO MYSQL (
  434. HOST 'mysql',
  435. USER mysql4,
  436. PASSWORD SECRET mysql_invisible_pass
  437. )
  438. > CREATE SOURCE mysql_invisible_source
  439. FROM MYSQL CONNECTION mysql_invisible_conn;
  440. > CREATE TABLE mysql_invisible_table FROM SOURCE mysql_invisible_source (REFERENCE public.mysql_invisible_table);
  441. # Return all rows
  442. > CREATE MATERIALIZED VIEW mysql_invisible_view AS
  443. SELECT * FROM mysql_invisible_table
  444. """
  445. )
  446. )
  447. def manipulate(self) -> list[Testdrive]:
  448. return [
  449. Testdrive(dedent(s))
  450. for s in [
  451. f"""
  452. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  453. $ mysql-execute name=mysql
  454. USE public;
  455. INSERT INTO mysql_invisible_table (f1, f2, f3, f4) VALUES (2, 0.2, '2025-02-02', 'two');
  456. """,
  457. f"""
  458. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  459. $ mysql-execute name=mysql
  460. USE public;
  461. INSERT INTO mysql_invisible_table (f1, f2, f3, f4) VALUES (3, 0.3, '2025-03-03', 'three');
  462. """,
  463. ]
  464. ]
  465. def validate(self) -> Testdrive:
  466. return Testdrive(
  467. dedent(
  468. f"""
  469. > SELECT * FROM mysql_invisible_table;
  470. 1 0.1 2025-01-01 one
  471. 2 0.2 2025-02-02 two
  472. 3 0.3 2025-03-03 three
  473. $ mysql-connect name=mysql url=mysql://root@mysql password={MySql.DEFAULT_ROOT_PASSWORD}
  474. $ mysql-execute name=mysql
  475. USE public;
  476. ALTER TABLE mysql_invisible_table ALTER COLUMN f2 SET VISIBLE;
  477. INSERT INTO mysql_invisible_table (f1, f2, f3, f4) VALUES (4, 0.4, '2025-04-04', 'four');
  478. > SELECT * FROM mysql_invisible_table;
  479. 1 0.1 2025-01-01 one
  480. 2 0.2 2025-02-02 two
  481. 3 0.3 2025-03-03 three
  482. 4 0.4 2025-04-04 four
  483. # Rollback the last INSERTs so that validate() can be called multiple times
  484. $ mysql-execute name=mysql
  485. DELETE FROM mysql_invisible_table WHERE f1 = 4;
  486. ALTER TABLE mysql_invisible_table ALTER COLUMN f2 SET INVISIBLE;
  487. > SELECT * FROM mysql_invisible_table;
  488. 1 0.1 2025-01-01 one
  489. 2 0.2 2025-02-02 two
  490. 3 0.3 2025-03-03 three
  491. """
  492. )
  493. )
  494. def remove_target_cluster_from_explain(sql: str) -> str:
  495. return re.sub(r"\n\s*Target cluster: \w+\n", "", sql)