v0.3.4-pre.003
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/schema.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
/// Immutable V000 physical schema resource inventory.
|
||||
pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource {
|
||||
@@ -370,7 +370,7 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
|
||||
access_method: "btree",
|
||||
key_fragment: "slot,signature",
|
||||
name: "ix_ksp_raw_transactions_slot_signature",
|
||||
predicate_fragment: "retention_state<>'purged'",
|
||||
predicate_fragment: std::option::Option::Some("retention_state<>'purged'"),
|
||||
table: "ksp_raw_transactions",
|
||||
unique: false,
|
||||
}),
|
||||
@@ -378,7 +378,7 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
|
||||
sql: include_str!("../migrations/v001_raw_transaction/indexes/001_ix_ksp_raw_transactions_slot_signature.sql"),
|
||||
},
|
||||
];
|
||||
/// V002 physical schema resource inventory for the minimal RAW account state foundation.
|
||||
/// Final V002 physical schema resource inventory for RAW account state persistence.
|
||||
pub(crate) const V002_RESOURCES: &[SchemaResource] = &[
|
||||
SchemaResource {
|
||||
id: "tables/001_ksp_raw_account_states.sql",
|
||||
@@ -426,6 +426,259 @@ pub(crate) const V002_RESOURCES: &[SchemaResource] = &[
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/004_ck_ksp_raw_account_states_pubkey.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_pubkey", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/005_ck_ksp_raw_account_states_slot.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_slot", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/006_ck_ksp_raw_account_states_state_hash.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_states_state_hash",
|
||||
table: "ksp_raw_account_states",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/007_ck_ksp_raw_account_states_lamports.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_lamports", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/008_ck_ksp_raw_account_states_owner.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_owner", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/009_ck_ksp_raw_account_states_rent_epoch.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_states_rent_epoch",
|
||||
table: "ksp_raw_account_states",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/010_ck_ksp_raw_account_states_data.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_data", table: "ksp_raw_account_states" }),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/011_ck_ksp_raw_account_observations_key.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_key",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_account_pubkey",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/013_ck_ksp_raw_account_observations_account_slot.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_account_slot",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_account_state_hash",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/015_ck_ksp_raw_account_observations_provider.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_provider",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/016_ck_ksp_raw_account_observations_protocol.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_protocol",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/017_ck_ksp_raw_account_observations_method.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_method",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/018_ck_ksp_raw_account_observations_origin.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_origin",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/019_ck_ksp_raw_account_observations_received_at.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_received_at",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/020_ck_ksp_raw_account_observations_capture_session.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_capture_session",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/021_ck_ksp_raw_account_observations_commitment.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_commitment",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/022_ck_ksp_raw_account_observations_endpoint.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_endpoint",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/023_ck_ksp_raw_account_observations_filter.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_filter",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/024_ck_ksp_raw_account_observations_observed_at.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_observed_at",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/025_ck_ksp_raw_account_observations_time_order.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_time_order",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/026_ck_ksp_raw_account_observations_source_hash.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_source_hash",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/027_ck_ksp_raw_account_observations_source_size.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_source_size",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_transaction_signature",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "constraints/029_ck_ksp_raw_account_observations_write_version.sql",
|
||||
object: SchemaObjectContract::Constraint(ConstraintContract {
|
||||
kind: "c",
|
||||
name: "ck_ksp_raw_account_observations_write_version",
|
||||
table: "ksp_raw_account_observations",
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql"),
|
||||
},
|
||||
SchemaResource {
|
||||
id: "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql",
|
||||
object: SchemaObjectContract::Index(IndexContract {
|
||||
access_method: "btree",
|
||||
key_fragment: "slot,pubkey,state_hash",
|
||||
name: "ix_ksp_raw_account_states_slot_pubkey_state_hash",
|
||||
predicate_fragment: std::option::Option::None,
|
||||
table: "ksp_raw_account_states",
|
||||
unique: false,
|
||||
}),
|
||||
repair_existing: true,
|
||||
sql: include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql"),
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMN_LOAD_SQL: &str = r#"SELECT column_name::TEXT, udt_name::TEXT, (is_nullable = 'YES') AS nullable, numeric_precision::INTEGER, numeric_scale::INTEGER, column_default::TEXT, is_identity::TEXT, is_generated::TEXT
|
||||
@@ -453,7 +706,7 @@ WHERE ns.nspname = current_schema() AND table_rel.relname = $1 AND index_rel.rel
|
||||
const MANAGED_OBJECT_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads')
|
||||
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads', 'ksp_raw_account_states', 'ksp_raw_account_observations')
|
||||
AND table_type = 'BASE TABLE'
|
||||
)"#;
|
||||
const PRIMARY_KEY_LOAD_SQL: &str = r#"SELECT string_agg(att.attname::TEXT, ',' ORDER BY key_part.ord)::TEXT
|
||||
@@ -513,7 +766,7 @@ struct IndexContract {
|
||||
access_method: &'static str,
|
||||
key_fragment: &'static str,
|
||||
name: &'static str,
|
||||
predicate_fragment: &'static str,
|
||||
predicate_fragment: std::option::Option<&'static str>,
|
||||
table: &'static str,
|
||||
unique: bool,
|
||||
}
|
||||
@@ -969,6 +1222,7 @@ const RAW_TRANSACTION_ARCHIVE_PAYLOADS_COLUMNS: &[ColumnContract] = &[
|
||||
},
|
||||
];
|
||||
const V001_TABLE_NAMES: &[&str] = &["ksp_store_identity", "ksp_raw_transactions", "ksp_raw_transaction_observations", "ksp_raw_transaction_archive_payloads"];
|
||||
const V002_TABLE_NAMES: &[&str] = &["ksp_raw_account_states", "ksp_raw_account_observations"];
|
||||
|
||||
/// Inspects one embedded schema resource against the effective PostgreSQL catalog.
|
||||
pub(crate) async fn inspect_resource(
|
||||
@@ -982,8 +1236,8 @@ pub(crate) async fn inspect_resource(
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns whether any V001-managed base table already exists in the active schema.
|
||||
pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
|
||||
/// Returns whether any KSP-managed Store base table already exists in the active schema.
|
||||
pub(crate) async fn managed_schema_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
|
||||
let result = transaction.query_one(MANAGED_OBJECT_EXISTS_SQL, &[]).await;
|
||||
let row = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -999,7 +1253,22 @@ pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::
|
||||
pub(crate) async fn verify_v001_external_compatibility(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
for table in V001_TABLE_NAMES {
|
||||
return verify_external_compatibility(transaction, V001_TABLE_NAMES, V001_RESOURCES).await;
|
||||
}
|
||||
|
||||
/// Rejects external V002 schema extensions that can constrain or mutate KSP writes.
|
||||
pub(crate) async fn verify_v002_external_compatibility(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
return verify_external_compatibility(transaction, V002_TABLE_NAMES, V002_RESOURCES).await;
|
||||
}
|
||||
|
||||
async fn verify_external_compatibility(
|
||||
transaction: &deadpool_postgres::Transaction<'_>,
|
||||
tables: &[&str],
|
||||
resources: &[SchemaResource],
|
||||
) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
for table in tables {
|
||||
let table = *table;
|
||||
let constraint_rows = transaction.query(UNEXPECTED_CONSTRAINTS_SQL, &[&table]).await;
|
||||
let constraint_rows = match constraint_rows {
|
||||
@@ -1024,11 +1293,11 @@ pub(crate) async fn verify_v001_external_compatibility(
|
||||
) => (name, kind, validated, deferrable, deferred, definition),
|
||||
_ => return schema_query_error("schema_constraint_inventory_decode"),
|
||||
};
|
||||
if is_expected_constraint(table, name.as_str()) {
|
||||
if is_expected_constraint(resources, table, name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let definition = normalize_catalog_sql(definition.as_str());
|
||||
if !validated || deferrable || deferred || !matches_expected_constraint_definition(table, kind.as_str(), definition.as_str()) {
|
||||
if !validated || deferrable || deferred || !matches_expected_constraint_definition(resources, table, kind.as_str(), definition.as_str()) {
|
||||
return schema_incompatible("schema_external_constraint");
|
||||
}
|
||||
}
|
||||
@@ -1129,9 +1398,10 @@ async fn inspect_index(
|
||||
};
|
||||
let definition = normalize_catalog_sql(definition.as_str());
|
||||
let predicate = predicate.map(|value| return normalize_catalog_sql(value.as_str()));
|
||||
let predicate_matches = match predicate.as_deref() {
|
||||
std::option::Option::Some(value) => value.contains(contract.predicate_fragment),
|
||||
std::option::Option::None => false,
|
||||
let predicate_matches = match (contract.predicate_fragment, predicate.as_deref()) {
|
||||
(std::option::Option::Some(expected), std::option::Option::Some(value)) => value.contains(expected),
|
||||
(std::option::Option::None, std::option::Option::None) => true,
|
||||
(std::option::Option::Some(_), std::option::Option::None) | (std::option::Option::None, std::option::Option::Some(_)) => false,
|
||||
};
|
||||
if unique != contract.unique || access_method != contract.access_method || !definition.contains(contract.key_fragment) || !predicate_matches {
|
||||
return std::result::Result::Ok(SchemaResourceState::Incompatible);
|
||||
@@ -1347,8 +1617,8 @@ fn expected_constraint_definition(resource_sql: &str, name: &str) -> std::option
|
||||
return std::option::Option::Some(normalize_catalog_sql(definition));
|
||||
}
|
||||
|
||||
fn is_expected_constraint(table: &str, name: &str) -> bool {
|
||||
for resource in V001_RESOURCES {
|
||||
fn is_expected_constraint(resources: &[SchemaResource], table: &str, name: &str) -> bool {
|
||||
for resource in resources {
|
||||
let contract = match resource.object {
|
||||
SchemaObjectContract::Constraint(value) => value,
|
||||
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,
|
||||
@@ -1360,8 +1630,8 @@ fn is_expected_constraint(table: &str, name: &str) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn matches_expected_constraint_definition(table: &str, kind: &str, definition: &str) -> bool {
|
||||
for resource in V001_RESOURCES {
|
||||
fn matches_expected_constraint_definition(resources: &[SchemaResource], table: &str, kind: &str, definition: &str) -> bool {
|
||||
for resource in resources {
|
||||
let contract = match resource.object {
|
||||
SchemaObjectContract::Constraint(value) => value,
|
||||
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,
|
||||
|
||||
Reference in New Issue
Block a user