Feat v0.5.4 package updates #34

Merged
xcaliber merged 7 commits from feat/v0.5.4-package-updates into main 2026-03-30 19:31:46 +00:00
Showing only changes of commit ecaeb8bec4 - Show all commits

View File

@@ -186,6 +186,130 @@ func CreateExtTables(
return nil
}
// ListExtColumns returns the set of column names for a physical table.
// Uses PRAGMA table_info on SQLite and information_schema on Postgres.
func ListExtColumns(ctx context.Context, db *sql.DB, isPostgres bool, physicalTable string) (map[string]bool, error) {
cols := make(map[string]bool)
var rows *sql.Rows
var err error
if isPostgres {
rows, err = db.QueryContext(ctx,
`SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public'`,
physicalTable)
} else {
rows, err = db.QueryContext(ctx, fmt.Sprintf("PRAGMA table_info(%s)", physicalTable))
}
if err != nil {
return nil, fmt.Errorf("list columns for %s: %w", physicalTable, err)
}
defer rows.Close()
for rows.Next() {
if isPostgres {
var name string
if err := rows.Scan(&name); err != nil {
continue
}
cols[name] = true
} else {
// PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
var cid int
var name, colType string
var notnull int
var dfltValue sql.NullString
var pk int
if err := rows.Scan(&cid, &name, &colType, &notnull, &dfltValue, &pk); err != nil {
continue
}
cols[name] = true
}
}
return cols, rows.Err()
}
// MigrateExtTables applies additive-only schema changes for a package update.
// For each table in the new manifest:
// - New table: CREATE TABLE (full creation, same as CreateExtTables)
// - Existing table: ALTER TABLE ADD COLUMN for each new column
//
// Indexes are applied idempotently with CREATE INDEX IF NOT EXISTS.
// New tables are registered in the ext_data_tables catalog.
// Returns a log of changes applied (for API response).
//
// No destructive changes: columns present in DB but absent from manifest are NOT dropped.
func MigrateExtTables(
ctx context.Context,
db *sql.DB,
isPostgres bool,
stores store.Stores,
packageID string,
newTables map[string]TableDef,
) ([]string, error) {
if db == nil {
return nil, nil
}
// Get existing registered tables for this package
existingTableNames := make(map[string]bool)
if stores.ExtData != nil {
names, err := stores.ExtData.ListTables(ctx, packageID)
if err != nil {
return nil, fmt.Errorf("list existing tables for %s: %w", packageID, err)
}
for _, n := range names {
existingTableNames[n] = true
}
}
var changes []string
for logicalName, td := range newTables {
physical := extPhysicalTable(packageID, logicalName)
if !existingTableNames[logicalName] {
// New table — full creation
if err := CreateExtTables(ctx, db, isPostgres, stores, packageID,
map[string]TableDef{logicalName: td}); err != nil {
return changes, fmt.Errorf("create new table %s: %w", physical, err)
}
changes = append(changes, fmt.Sprintf("created table %s", physical))
continue
}
// Existing table — diff columns and add missing ones
existingCols, err := ListExtColumns(ctx, db, isPostgres, physical)
if err != nil {
return changes, err
}
for col, typ := range td.Columns {
if existingCols[col] {
continue
}
sqlType := mapColType(typ, isPostgres)
alterDDL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", physical, col, sqlType)
if _, err := db.ExecContext(ctx, alterDDL); err != nil {
return changes, fmt.Errorf("add column %s.%s: %w", physical, col, err)
}
changes = append(changes, fmt.Sprintf("added column '%s' to %s", col, physical))
log.Printf("[ext-db] added column %s (%s) to %s", col, sqlType, physical)
}
// Re-apply indexes idempotently
for i, idxCols := range td.Indexes {
idxName := fmt.Sprintf("idx_%s_%d", physical, i)
idxDDL := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s (%s)",
idxName, physical, strings.Join(idxCols, ", "))
if _, err := db.ExecContext(ctx, idxDDL); err != nil {
log.Printf("[ext-db] index create failed (%s): %v", idxName, err)
}
}
}
return changes, nil
}
// DropExtTables drops all physical tables registered for a package and removes
// their entries from the ext_data_tables catalog.
//