misc.python.materialize.zippy.table_actions
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 10import random 11from textwrap import dedent 12 13from materialize.mzcompose.composition import Composition 14from materialize.zippy.balancerd_capabilities import BalancerdIsRunning 15from materialize.zippy.framework import ( 16 Action, 17 ActionFactory, 18 Capabilities, 19 Capability, 20 State, 21) 22from materialize.zippy.mz_capabilities import MzIsRunning 23from materialize.zippy.table_capabilities import TableExists 24from materialize.zippy.view_capabilities import ViewExists 25 26MAX_ROWS_PER_ACTION = 10000 27 28 29class CreateTableParameterized(ActionFactory): 30 def __init__( 31 self, max_tables: int = 10, max_rows_per_action: int = MAX_ROWS_PER_ACTION 32 ) -> None: 33 self.max_tables = max_tables 34 self.max_rows_per_action = max_rows_per_action 35 36 @classmethod 37 def requires(cls) -> set[type[Capability]]: 38 return {BalancerdIsRunning, MzIsRunning} 39 40 def new(self, capabilities: Capabilities) -> list[Action]: 41 new_table_name = capabilities.get_free_capability_name( 42 TableExists, self.max_tables 43 ) 44 45 if new_table_name: 46 return [ 47 CreateTable( 48 capabilities=capabilities, 49 table=TableExists( 50 name=new_table_name, 51 has_index=random.choice([True, False]), 52 max_rows_per_action=self.max_rows_per_action, 53 ), 54 ) 55 ] 56 else: 57 return [] 58 59 60class CreateTable(Action): 61 """Creates a table on the Mz instance. 50% of the tables have a default index.""" 62 63 @classmethod 64 def requires(cls) -> set[type[Capability]]: 65 return {BalancerdIsRunning, MzIsRunning} 66 67 def __init__(self, table: TableExists, capabilities: Capabilities) -> None: 68 assert ( 69 table is not None 70 ), "CreateTable Action can not be referenced directly, it is produced by CreateTableParameterized factory" 71 self.table = table 72 super().__init__(capabilities) 73 74 def run(self, c: Composition, state: State) -> None: 75 index = ( 76 f"> CREATE DEFAULT INDEX ON {self.table.name}" 77 if self.table.has_index 78 else "" 79 ) 80 c.testdrive( 81 dedent(f""" 82 > CREATE TABLE {self.table.name} (f1 INTEGER); 83 {index} 84 > INSERT INTO {self.table.name} VALUES ({self.table.watermarks.max}); 85 """), 86 mz_service=state.mz_service, 87 ) 88 89 def provides(self) -> list[Capability]: 90 return [self.table] 91 92 93class ValidateTable(Action): 94 """Validates that a single table contains data that is consistent with the expected min/max watermark.""" 95 96 @classmethod 97 def requires(cls) -> set[type[Capability]]: 98 return {BalancerdIsRunning, MzIsRunning, TableExists} 99 100 def __init__( 101 self, capabilities: Capabilities, table: TableExists | None = None 102 ) -> None: 103 if table is not None: 104 self.table = table 105 else: 106 self.table = random.choice(capabilities.get(TableExists)) 107 108 self.select_limit = random.choices([True, False], weights=[0.2, 0.8], k=1)[0] 109 super().__init__(capabilities) 110 111 def run(self, c: Composition, state: State) -> None: 112 # Validating via SELECT ... LIMIT is expensive as it requires creating a temporary table 113 # Therefore, only use it in 20% of validations. 114 if self.select_limit: 115 c.testdrive( 116 dedent(f""" 117 > CREATE TEMPORARY TABLE {self.table.name}_select_limit (f1 INTEGER); 118 > INSERT INTO {self.table.name}_select_limit SELECT * FROM {self.table.name} LIMIT 999999999; 119 > SELECT MIN(f1), MAX(f1), COUNT(f1), COUNT(DISTINCT f1) FROM {self.table.name}_select_limit; 120 {self.table.watermarks.min} {self.table.watermarks.max} {(self.table.watermarks.max-self.table.watermarks.min)+1} {(self.table.watermarks.max-self.table.watermarks.min)+1} 121 > DROP TABLE {self.table.name}_select_limit 122 """), 123 mz_service=state.mz_service, 124 ) 125 else: 126 c.testdrive( 127 dedent(f""" 128 > SELECT MIN(f1), MAX(f1), COUNT(f1), COUNT(DISTINCT f1) FROM {self.table.name}; 129 {self.table.watermarks.min} {self.table.watermarks.max} {(self.table.watermarks.max-self.table.watermarks.min)+1} {(self.table.watermarks.max-self.table.watermarks.min)+1} 130 """), 131 mz_service=state.mz_service, 132 ) 133 134 135class DropTable(Action): 136 """Drops a table that no view reads from.""" 137 138 @classmethod 139 def requires(cls) -> set[type[Capability]]: 140 return {BalancerdIsRunning, MzIsRunning, TableExists} 141 142 def __init__(self, capabilities: Capabilities) -> None: 143 referenced: set[str] = set() 144 for view in capabilities.get(ViewExists): 145 referenced.update(input.name for input in view.inputs) 146 147 candidates = [ 148 t for t in capabilities.get(TableExists) if t.name not in referenced 149 ] 150 self.table: TableExists | None = ( 151 random.choice(candidates) if candidates else None 152 ) 153 if self.table is not None: 154 capabilities.remove_capability_instance(self.table) 155 super().__init__(capabilities) 156 157 def __str__(self) -> str: 158 return f"{Action.__str__(self)} {self.table.name if self.table else '<none>'}" 159 160 def run(self, c: Composition, state: State) -> None: 161 if self.table is not None: 162 c.testdrive( 163 f"> DROP TABLE {self.table.name};", 164 mz_service=state.mz_service, 165 ) 166 167 168class DML(Action): 169 """Performs an INSERT, DELETE or UPDATE against a table.""" 170 171 @classmethod 172 def requires(cls) -> set[type[Capability]]: 173 return {BalancerdIsRunning, MzIsRunning, TableExists} 174 175 def __init__(self, capabilities: Capabilities) -> None: 176 self.table = random.choice(capabilities.get(TableExists)) 177 self.delta = random.randint(1, self.table.max_rows_per_action) 178 super().__init__(capabilities) 179 180 def __str__(self) -> str: 181 return f"{Action.__str__(self)} {self.table.name}" 182 183 184class Insert(DML): 185 """Inserts rows into a table.""" 186 187 def run(self, c: Composition, state: State) -> None: 188 prev_max = self.table.watermarks.max 189 self.table.watermarks.max = prev_max + self.delta 190 c.testdrive( 191 f"> INSERT INTO {self.table.name} SELECT * FROM generate_series({prev_max + 1}, {self.table.watermarks.max});", 192 mz_service=state.mz_service, 193 ) 194 195 196class ShiftForward(DML): 197 """Update all rows from a table by incrementing their values by a constant.""" 198 199 def run(self, c: Composition, state: State) -> None: 200 self.table.watermarks.shift(self.delta) 201 c.testdrive( 202 f"> UPDATE {self.table.name} SET f1 = f1 + {self.delta};", 203 mz_service=state.mz_service, 204 ) 205 206 207class ShiftBackward(DML): 208 """Update all rows from a table by decrementing their values by a constant.""" 209 210 def run(self, c: Composition, state: State) -> None: 211 self.table.watermarks.shift(-self.delta) 212 c.testdrive( 213 f"> UPDATE {self.table.name} SET f1 = f1 - {self.delta};", 214 mz_service=state.mz_service, 215 ) 216 217 218class DeleteFromHead(DML): 219 """Delete the largest values from a table""" 220 221 def run(self, c: Composition, state: State) -> None: 222 self.table.watermarks.max = max( 223 self.table.watermarks.max - self.delta, self.table.watermarks.min 224 ) 225 c.testdrive( 226 f"> DELETE FROM {self.table.name} WHERE f1 > {self.table.watermarks.max};", 227 mz_service=state.mz_service, 228 ) 229 230 231class DeleteFromTail(DML): 232 """Delete the smallest values from a table""" 233 234 def run(self, c: Composition, state: State) -> None: 235 self.table.watermarks.min = min( 236 self.table.watermarks.min + self.delta, self.table.watermarks.max 237 ) 238 c.testdrive( 239 f"> DELETE FROM {self.table.name} WHERE f1 < {self.table.watermarks.min};", 240 mz_service=state.mz_service, 241 )
30class CreateTableParameterized(ActionFactory): 31 def __init__( 32 self, max_tables: int = 10, max_rows_per_action: int = MAX_ROWS_PER_ACTION 33 ) -> None: 34 self.max_tables = max_tables 35 self.max_rows_per_action = max_rows_per_action 36 37 @classmethod 38 def requires(cls) -> set[type[Capability]]: 39 return {BalancerdIsRunning, MzIsRunning} 40 41 def new(self, capabilities: Capabilities) -> list[Action]: 42 new_table_name = capabilities.get_free_capability_name( 43 TableExists, self.max_tables 44 ) 45 46 if new_table_name: 47 return [ 48 CreateTable( 49 capabilities=capabilities, 50 table=TableExists( 51 name=new_table_name, 52 has_index=random.choice([True, False]), 53 max_rows_per_action=self.max_rows_per_action, 54 ), 55 ) 56 ] 57 else: 58 return []
Base class for Action Factories that return parameterized Actions to execute.
37 @classmethod 38 def requires(cls) -> set[type[Capability]]: 39 return {BalancerdIsRunning, MzIsRunning}
Compute the capability classes that this Action Factory requires.
41 def new(self, capabilities: Capabilities) -> list[Action]: 42 new_table_name = capabilities.get_free_capability_name( 43 TableExists, self.max_tables 44 ) 45 46 if new_table_name: 47 return [ 48 CreateTable( 49 capabilities=capabilities, 50 table=TableExists( 51 name=new_table_name, 52 has_index=random.choice([True, False]), 53 max_rows_per_action=self.max_rows_per_action, 54 ), 55 ) 56 ] 57 else: 58 return []
61class CreateTable(Action): 62 """Creates a table on the Mz instance. 50% of the tables have a default index.""" 63 64 @classmethod 65 def requires(cls) -> set[type[Capability]]: 66 return {BalancerdIsRunning, MzIsRunning} 67 68 def __init__(self, table: TableExists, capabilities: Capabilities) -> None: 69 assert ( 70 table is not None 71 ), "CreateTable Action can not be referenced directly, it is produced by CreateTableParameterized factory" 72 self.table = table 73 super().__init__(capabilities) 74 75 def run(self, c: Composition, state: State) -> None: 76 index = ( 77 f"> CREATE DEFAULT INDEX ON {self.table.name}" 78 if self.table.has_index 79 else "" 80 ) 81 c.testdrive( 82 dedent(f""" 83 > CREATE TABLE {self.table.name} (f1 INTEGER); 84 {index} 85 > INSERT INTO {self.table.name} VALUES ({self.table.watermarks.max}); 86 """), 87 mz_service=state.mz_service, 88 ) 89 90 def provides(self) -> list[Capability]: 91 return [self.table]
Creates a table on the Mz instance. 50% of the tables have a default index.
68 def __init__(self, table: TableExists, capabilities: Capabilities) -> None: 69 assert ( 70 table is not None 71 ), "CreateTable Action can not be referenced directly, it is produced by CreateTableParameterized factory" 72 self.table = table 73 super().__init__(capabilities)
Construct a new action, possibly conditioning on the available capabilities.
64 @classmethod 65 def requires(cls) -> set[type[Capability]]: 66 return {BalancerdIsRunning, MzIsRunning}
Compute the capability classes that this action requires.
75 def run(self, c: Composition, state: State) -> None: 76 index = ( 77 f"> CREATE DEFAULT INDEX ON {self.table.name}" 78 if self.table.has_index 79 else "" 80 ) 81 c.testdrive( 82 dedent(f""" 83 > CREATE TABLE {self.table.name} (f1 INTEGER); 84 {index} 85 > INSERT INTO {self.table.name} VALUES ({self.table.watermarks.max}); 86 """), 87 mz_service=state.mz_service, 88 )
Run this action on the provided composition.
94class ValidateTable(Action): 95 """Validates that a single table contains data that is consistent with the expected min/max watermark.""" 96 97 @classmethod 98 def requires(cls) -> set[type[Capability]]: 99 return {BalancerdIsRunning, MzIsRunning, TableExists} 100 101 def __init__( 102 self, capabilities: Capabilities, table: TableExists | None = None 103 ) -> None: 104 if table is not None: 105 self.table = table 106 else: 107 self.table = random.choice(capabilities.get(TableExists)) 108 109 self.select_limit = random.choices([True, False], weights=[0.2, 0.8], k=1)[0] 110 super().__init__(capabilities) 111 112 def run(self, c: Composition, state: State) -> None: 113 # Validating via SELECT ... LIMIT is expensive as it requires creating a temporary table 114 # Therefore, only use it in 20% of validations. 115 if self.select_limit: 116 c.testdrive( 117 dedent(f""" 118 > CREATE TEMPORARY TABLE {self.table.name}_select_limit (f1 INTEGER); 119 > INSERT INTO {self.table.name}_select_limit SELECT * FROM {self.table.name} LIMIT 999999999; 120 > SELECT MIN(f1), MAX(f1), COUNT(f1), COUNT(DISTINCT f1) FROM {self.table.name}_select_limit; 121 {self.table.watermarks.min} {self.table.watermarks.max} {(self.table.watermarks.max-self.table.watermarks.min)+1} {(self.table.watermarks.max-self.table.watermarks.min)+1} 122 > DROP TABLE {self.table.name}_select_limit 123 """), 124 mz_service=state.mz_service, 125 ) 126 else: 127 c.testdrive( 128 dedent(f""" 129 > SELECT MIN(f1), MAX(f1), COUNT(f1), COUNT(DISTINCT f1) FROM {self.table.name}; 130 {self.table.watermarks.min} {self.table.watermarks.max} {(self.table.watermarks.max-self.table.watermarks.min)+1} {(self.table.watermarks.max-self.table.watermarks.min)+1} 131 """), 132 mz_service=state.mz_service, 133 )
Validates that a single table contains data that is consistent with the expected min/max watermark.
101 def __init__( 102 self, capabilities: Capabilities, table: TableExists | None = None 103 ) -> None: 104 if table is not None: 105 self.table = table 106 else: 107 self.table = random.choice(capabilities.get(TableExists)) 108 109 self.select_limit = random.choices([True, False], weights=[0.2, 0.8], k=1)[0] 110 super().__init__(capabilities)
Construct a new action, possibly conditioning on the available capabilities.
97 @classmethod 98 def requires(cls) -> set[type[Capability]]: 99 return {BalancerdIsRunning, MzIsRunning, TableExists}
Compute the capability classes that this action requires.
112 def run(self, c: Composition, state: State) -> None: 113 # Validating via SELECT ... LIMIT is expensive as it requires creating a temporary table 114 # Therefore, only use it in 20% of validations. 115 if self.select_limit: 116 c.testdrive( 117 dedent(f""" 118 > CREATE TEMPORARY TABLE {self.table.name}_select_limit (f1 INTEGER); 119 > INSERT INTO {self.table.name}_select_limit SELECT * FROM {self.table.name} LIMIT 999999999; 120 > SELECT MIN(f1), MAX(f1), COUNT(f1), COUNT(DISTINCT f1) FROM {self.table.name}_select_limit; 121 {self.table.watermarks.min} {self.table.watermarks.max} {(self.table.watermarks.max-self.table.watermarks.min)+1} {(self.table.watermarks.max-self.table.watermarks.min)+1} 122 > DROP TABLE {self.table.name}_select_limit 123 """), 124 mz_service=state.mz_service, 125 ) 126 else: 127 c.testdrive( 128 dedent(f""" 129 > SELECT MIN(f1), MAX(f1), COUNT(f1), COUNT(DISTINCT f1) FROM {self.table.name}; 130 {self.table.watermarks.min} {self.table.watermarks.max} {(self.table.watermarks.max-self.table.watermarks.min)+1} {(self.table.watermarks.max-self.table.watermarks.min)+1} 131 """), 132 mz_service=state.mz_service, 133 )
Run this action on the provided composition.
136class DropTable(Action): 137 """Drops a table that no view reads from.""" 138 139 @classmethod 140 def requires(cls) -> set[type[Capability]]: 141 return {BalancerdIsRunning, MzIsRunning, TableExists} 142 143 def __init__(self, capabilities: Capabilities) -> None: 144 referenced: set[str] = set() 145 for view in capabilities.get(ViewExists): 146 referenced.update(input.name for input in view.inputs) 147 148 candidates = [ 149 t for t in capabilities.get(TableExists) if t.name not in referenced 150 ] 151 self.table: TableExists | None = ( 152 random.choice(candidates) if candidates else None 153 ) 154 if self.table is not None: 155 capabilities.remove_capability_instance(self.table) 156 super().__init__(capabilities) 157 158 def __str__(self) -> str: 159 return f"{Action.__str__(self)} {self.table.name if self.table else '<none>'}" 160 161 def run(self, c: Composition, state: State) -> None: 162 if self.table is not None: 163 c.testdrive( 164 f"> DROP TABLE {self.table.name};", 165 mz_service=state.mz_service, 166 )
Drops a table that no view reads from.
143 def __init__(self, capabilities: Capabilities) -> None: 144 referenced: set[str] = set() 145 for view in capabilities.get(ViewExists): 146 referenced.update(input.name for input in view.inputs) 147 148 candidates = [ 149 t for t in capabilities.get(TableExists) if t.name not in referenced 150 ] 151 self.table: TableExists | None = ( 152 random.choice(candidates) if candidates else None 153 ) 154 if self.table is not None: 155 capabilities.remove_capability_instance(self.table) 156 super().__init__(capabilities)
Construct a new action, possibly conditioning on the available capabilities.
139 @classmethod 140 def requires(cls) -> set[type[Capability]]: 141 return {BalancerdIsRunning, MzIsRunning, TableExists}
Compute the capability classes that this action requires.
161 def run(self, c: Composition, state: State) -> None: 162 if self.table is not None: 163 c.testdrive( 164 f"> DROP TABLE {self.table.name};", 165 mz_service=state.mz_service, 166 )
Run this action on the provided composition.
169class DML(Action): 170 """Performs an INSERT, DELETE or UPDATE against a table.""" 171 172 @classmethod 173 def requires(cls) -> set[type[Capability]]: 174 return {BalancerdIsRunning, MzIsRunning, TableExists} 175 176 def __init__(self, capabilities: Capabilities) -> None: 177 self.table = random.choice(capabilities.get(TableExists)) 178 self.delta = random.randint(1, self.table.max_rows_per_action) 179 super().__init__(capabilities) 180 181 def __str__(self) -> str: 182 return f"{Action.__str__(self)} {self.table.name}"
Performs an INSERT, DELETE or UPDATE against a table.
176 def __init__(self, capabilities: Capabilities) -> None: 177 self.table = random.choice(capabilities.get(TableExists)) 178 self.delta = random.randint(1, self.table.max_rows_per_action) 179 super().__init__(capabilities)
Construct a new action, possibly conditioning on the available capabilities.
185class Insert(DML): 186 """Inserts rows into a table.""" 187 188 def run(self, c: Composition, state: State) -> None: 189 prev_max = self.table.watermarks.max 190 self.table.watermarks.max = prev_max + self.delta 191 c.testdrive( 192 f"> INSERT INTO {self.table.name} SELECT * FROM generate_series({prev_max + 1}, {self.table.watermarks.max});", 193 mz_service=state.mz_service, 194 )
Inserts rows into a table.
188 def run(self, c: Composition, state: State) -> None: 189 prev_max = self.table.watermarks.max 190 self.table.watermarks.max = prev_max + self.delta 191 c.testdrive( 192 f"> INSERT INTO {self.table.name} SELECT * FROM generate_series({prev_max + 1}, {self.table.watermarks.max});", 193 mz_service=state.mz_service, 194 )
Run this action on the provided composition.
197class ShiftForward(DML): 198 """Update all rows from a table by incrementing their values by a constant.""" 199 200 def run(self, c: Composition, state: State) -> None: 201 self.table.watermarks.shift(self.delta) 202 c.testdrive( 203 f"> UPDATE {self.table.name} SET f1 = f1 + {self.delta};", 204 mz_service=state.mz_service, 205 )
Update all rows from a table by incrementing their values by a constant.
200 def run(self, c: Composition, state: State) -> None: 201 self.table.watermarks.shift(self.delta) 202 c.testdrive( 203 f"> UPDATE {self.table.name} SET f1 = f1 + {self.delta};", 204 mz_service=state.mz_service, 205 )
Run this action on the provided composition.
208class ShiftBackward(DML): 209 """Update all rows from a table by decrementing their values by a constant.""" 210 211 def run(self, c: Composition, state: State) -> None: 212 self.table.watermarks.shift(-self.delta) 213 c.testdrive( 214 f"> UPDATE {self.table.name} SET f1 = f1 - {self.delta};", 215 mz_service=state.mz_service, 216 )
Update all rows from a table by decrementing their values by a constant.
211 def run(self, c: Composition, state: State) -> None: 212 self.table.watermarks.shift(-self.delta) 213 c.testdrive( 214 f"> UPDATE {self.table.name} SET f1 = f1 - {self.delta};", 215 mz_service=state.mz_service, 216 )
Run this action on the provided composition.
219class DeleteFromHead(DML): 220 """Delete the largest values from a table""" 221 222 def run(self, c: Composition, state: State) -> None: 223 self.table.watermarks.max = max( 224 self.table.watermarks.max - self.delta, self.table.watermarks.min 225 ) 226 c.testdrive( 227 f"> DELETE FROM {self.table.name} WHERE f1 > {self.table.watermarks.max};", 228 mz_service=state.mz_service, 229 )
Delete the largest values from a table
222 def run(self, c: Composition, state: State) -> None: 223 self.table.watermarks.max = max( 224 self.table.watermarks.max - self.delta, self.table.watermarks.min 225 ) 226 c.testdrive( 227 f"> DELETE FROM {self.table.name} WHERE f1 > {self.table.watermarks.max};", 228 mz_service=state.mz_service, 229 )
Run this action on the provided composition.
232class DeleteFromTail(DML): 233 """Delete the smallest values from a table""" 234 235 def run(self, c: Composition, state: State) -> None: 236 self.table.watermarks.min = min( 237 self.table.watermarks.min + self.delta, self.table.watermarks.max 238 ) 239 c.testdrive( 240 f"> DELETE FROM {self.table.name} WHERE f1 < {self.table.watermarks.min};", 241 mz_service=state.mz_service, 242 )
Delete the smallest values from a table
235 def run(self, c: Composition, state: State) -> None: 236 self.table.watermarks.min = min( 237 self.table.watermarks.min + self.delta, self.table.watermarks.max 238 ) 239 c.testdrive( 240 f"> DELETE FROM {self.table.name} WHERE f1 < {self.table.watermarks.min};", 241 mz_service=state.mz_service, 242 )
Run this action on the provided composition.