Skip to content

Commit 90ca105

Browse files
authored
feat(table): report the tables a schema load's filters excluded (#1241)
1 parent 0590676 commit 90ca105

2 files changed

Lines changed: 95 additions & 5 deletions

File tree

‎pkg/table/table_schema.go‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,48 +79,75 @@ func StripAutoIncrement(stmt string) string {
7979
return restored.String()
8080
}
8181

82+
// ExcludedTable is a table a filter option kept out of the schema
83+
// LoadSchemaAndExcludedTablesFromDB returned, paired with the option that
84+
// excluded it.
85+
type ExcludedTable struct {
86+
Name string // Table name
87+
Filter FilterOption // The filter option that excluded the table
88+
}
89+
8290
// LoadSchemaFromDB retrieves all table schemas from the database using the
8391
// provided connection. The returned tables and DDL are filtered according to
8492
// the supplied options. With no options the raw DDL is returned unmodified.
8593
func LoadSchemaFromDB(ctx context.Context, db *sql.DB, opts ...FilterOption) ([]TableSchema, error) {
94+
tables, _, err := LoadSchemaAndExcludedTablesFromDB(ctx, db, opts...)
95+
return tables, err
96+
}
97+
98+
// LoadSchemaAndExcludedTablesFromDB is LoadSchemaFromDB, additionally
99+
// reporting every table the filter options excluded and which option excluded
100+
// it. A caller that has to account for what is missing from the schema it
101+
// loaded — a declarative tool disclosing the tables it is declining to look
102+
// at, say — cannot recover those names from the returned schema, and
103+
// recovering them by re-implementing the predicates puts a second copy of them
104+
// outside this package, to drift the next time a filter changes.
105+
//
106+
// A table is reported once, under the first option that excluded it, and the
107+
// exclusions keep the order SHOW TABLES returned them in. WithStrippedAutoIncrement
108+
// rewrites DDL rather than excluding a table, so it never appears.
109+
func LoadSchemaAndExcludedTablesFromDB(ctx context.Context, db *sql.DB, opts ...FilterOption) ([]TableSchema, []ExcludedTable, error) {
86110
optSet := make(map[FilterOption]bool, len(opts))
87111
for _, o := range opts {
88112
optSet[o] = true
89113
}
90114

91115
rows, err := db.QueryContext(ctx, "SHOW TABLES")
92116
if err != nil {
93-
return nil, fmt.Errorf("failed to list tables: %w", err)
117+
return nil, nil, fmt.Errorf("failed to list tables: %w", err)
94118
}
95119
defer func() { _ = rows.Close() }()
96120
var tableNames []string
97121
for rows.Next() {
98122
var name string
99123
if err := rows.Scan(&name); err != nil {
100-
return nil, fmt.Errorf("failed to scan table name: %w", err)
124+
return nil, nil, fmt.Errorf("failed to scan table name: %w", err)
101125
}
102126
tableNames = append(tableNames, name)
103127
}
104128
if err := rows.Err(); err != nil {
105-
return nil, fmt.Errorf("error iterating tables: %w", err)
129+
return nil, nil, fmt.Errorf("error iterating tables: %w", err)
106130
}
107131
var tables []TableSchema
132+
var excluded []ExcludedTable
108133
for _, name := range tableNames {
109134
if optSet[WithoutUnderscoreTables] && strings.HasPrefix(name, "_") {
135+
excluded = append(excluded, ExcludedTable{Name: name, Filter: WithoutUnderscoreTables})
110136
continue
111137
}
112138
if optSet[WithoutArchiveTables] && IsArchiveTable(name) {
139+
excluded = append(excluded, ExcludedTable{Name: name, Filter: WithoutArchiveTables})
113140
continue
114141
}
115142
var tbl, createStmt string
116143
err := db.QueryRowContext(ctx, fmt.Sprintf("SHOW CREATE TABLE %s", sqlescape.EscapeIdentifier(name))).Scan(&tbl, &createStmt)
117144
if err != nil {
118-
return nil, fmt.Errorf("failed to get CREATE TABLE for %s: %w", name, err)
145+
return nil, nil, fmt.Errorf("failed to get CREATE TABLE for %s: %w", name, err)
119146
}
120147
if optSet[WithStrippedAutoIncrement] {
121148
createStmt = StripAutoIncrement(createStmt)
122149
}
123150
tables = append(tables, TableSchema{Name: tbl, Schema: createStmt})
124151
}
125-
return tables, nil
152+
return tables, excluded, nil
126153
}

‎pkg/table/table_schema_test.go‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,66 @@ func TestLoadSchemaFromDB_CombinedFilters(t *testing.T) {
173173
require.Equal(t, "users", filtered[0].Name)
174174
require.NotContains(t, filtered[0].Schema, "AUTO_INCREMENT=")
175175
}
176+
177+
func TestLoadSchemaAndExcludedTablesFromDB(t *testing.T) {
178+
dbName, _ := testutils.CreateUniqueTestDatabase(t)
179+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE users (
180+
id bigint unsigned NOT NULL AUTO_INCREMENT,
181+
PRIMARY KEY (id)
182+
) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=utf8mb4`)
183+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE _shadow (id bigint NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB`)
184+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE users_archive_2024 (id bigint NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB`)
185+
186+
db, err := sql.Open("block-mysql", testutils.DSNForDatabase(dbName))
187+
require.NoError(t, err)
188+
defer func() { _ = db.Close() }()
189+
190+
tables, excluded, err := LoadSchemaAndExcludedTablesFromDB(t.Context(), db,
191+
WithoutUnderscoreTables,
192+
WithoutArchiveTables,
193+
WithStrippedAutoIncrement,
194+
)
195+
require.NoError(t, err)
196+
require.Len(t, tables, 1)
197+
require.Equal(t, "users", tables[0].Name)
198+
require.NotContains(t, tables[0].Schema, "AUTO_INCREMENT=")
199+
200+
// Each excluded table is named alongside the option that excluded it, so a
201+
// caller can word the two exclusions differently.
202+
require.Equal(t, []ExcludedTable{
203+
{Name: "_shadow", Filter: WithoutUnderscoreTables},
204+
{Name: "users_archive_2024", Filter: WithoutArchiveTables},
205+
}, excluded)
206+
}
207+
208+
func TestLoadSchemaAndExcludedTablesFromDB_NoFilters(t *testing.T) {
209+
dbName, _ := testutils.CreateUniqueTestDatabase(t)
210+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE users (id bigint NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB`)
211+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE _shadow (id bigint NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB`)
212+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE users_archive_2024 (id bigint NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB`)
213+
214+
db, err := sql.Open("block-mysql", testutils.DSNForDatabase(dbName))
215+
require.NoError(t, err)
216+
defer func() { _ = db.Close() }()
217+
218+
tables, excluded, err := LoadSchemaAndExcludedTablesFromDB(t.Context(), db)
219+
require.NoError(t, err)
220+
require.Len(t, tables, 3)
221+
require.Empty(t, excluded)
222+
}
223+
224+
func TestLoadSchemaAndExcludedTablesFromDB_ReportsFirstMatchingFilter(t *testing.T) {
225+
// A name matching both conventions is excluded once, under the first
226+
// option that matched, so a caller never discloses one table twice.
227+
dbName, _ := testutils.CreateUniqueTestDatabase(t)
228+
testutils.RunSQLInDatabase(t, dbName, `CREATE TABLE _users_archive_2024 (id bigint NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB`)
229+
230+
db, err := sql.Open("block-mysql", testutils.DSNForDatabase(dbName))
231+
require.NoError(t, err)
232+
defer func() { _ = db.Close() }()
233+
234+
tables, excluded, err := LoadSchemaAndExcludedTablesFromDB(t.Context(), db, WithoutUnderscoreTables, WithoutArchiveTables)
235+
require.NoError(t, err)
236+
require.Empty(t, tables)
237+
require.Equal(t, []ExcludedTable{{Name: "_users_archive_2024", Filter: WithoutUnderscoreTables}}, excluded)
238+
}

0 commit comments

Comments
 (0)