-
Notifications
You must be signed in to change notification settings - Fork 3
feat(cli): implement phased plans for rollout and cutover #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import { latestByColumn } from '@cipherstash/migrate' | ||
| import type pg from 'pg' | ||
|
|
||
| /** | ||
| * `latestByColumn` from `@cipherstash/migrate`, but tolerant of the | ||
| * pre-install case where `cipherstash.cs_migrations` doesn't exist. | ||
| * The encryption-rollout commands all need to be readable on a fresh | ||
| * project; treating "table missing" as "no events" keeps them so. | ||
| */ | ||
| export async function latestByColumnSafe( | ||
| client: pg.ClientBase, | ||
| ): Promise<ReturnType<typeof latestByColumn> extends Promise<infer T> ? T : never> { | ||
| try { | ||
| return (await latestByColumn(client)) as Awaited<ReturnType<typeof latestByColumn>> | ||
| } catch (err) { | ||
| if ( | ||
| err instanceof Error && | ||
| /cs_migrations|schema "cipherstash"/i.test(err.message) | ||
| ) { | ||
| return new Map() as Awaited<ReturnType<typeof latestByColumn>> | ||
| } | ||
| throw err | ||
| } | ||
| } | ||
|
|
||
| export interface EqlColumnInfo { | ||
| /** Index kinds attached to this column in the EQL config (`unique`, | ||
| * `match`, `ore`, `ste_vec`). Empty when no indexes are configured. */ | ||
| indexes: string[] | ||
| /** Lifecycle state of the EQL config row this column belongs to. */ | ||
| state: 'active' | 'pending' | 'encrypting' | ||
| } | ||
|
|
||
| /** | ||
| * Read every column registered in `eql_v2_configuration` (active, | ||
| * pending, or encrypting) keyed by `<table>.<column>`. Active rows win | ||
| * when a column appears in more than one state. | ||
| * | ||
| * The call is best-effort: if `eql_v2_configuration` doesn't exist yet | ||
| * (EQL not installed), an empty map is returned instead of throwing. | ||
| */ | ||
| export async function fetchActiveEqlConfig( | ||
| client: pg.ClientBase, | ||
| ): Promise<Map<string, EqlColumnInfo>> { | ||
| const out = new Map<string, EqlColumnInfo>() | ||
| try { | ||
| const result = await client.query<{ state: string; data: unknown }>( | ||
| `SELECT state, data FROM public.eql_v2_configuration | ||
| WHERE state IN ('active', 'pending', 'encrypting') | ||
| ORDER BY CASE state WHEN 'active' THEN 0 WHEN 'encrypting' THEN 1 ELSE 2 END`, | ||
| ) | ||
| for (const row of result.rows) { | ||
| const data = row.data as { | ||
| tables?: Record< | ||
| string, | ||
| Record<string, { indexes?: Record<string, unknown> }> | ||
| > | ||
| } | null | ||
| if (!data?.tables) continue | ||
| for (const [tableName, columns] of Object.entries(data.tables)) { | ||
| for (const [columnName, column] of Object.entries(columns)) { | ||
| const key = `${tableName}.${columnName}` | ||
| if (out.has(key)) continue | ||
| out.set(key, { | ||
| indexes: Object.keys(column.indexes ?? {}), | ||
| state: row.state as 'active' | 'pending' | 'encrypting', | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| if (err instanceof Error && /eql_v2_configuration/i.test(err.message)) { | ||
| return out | ||
| } | ||
| throw err | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| /** | ||
| * Read `information_schema.columns` and group column names by table. | ||
| * When `tables` is provided the query is constrained to that set — | ||
| * status's quest log only ever needs ~5 specific tables, so passing | ||
| * the manifest's tables avoids a full-schema scan. | ||
| */ | ||
| export async function fetchPhysicalColumns( | ||
| client: pg.ClientBase, | ||
| tables?: ReadonlyArray<string>, | ||
| ): Promise<Map<string, Set<string>>> { | ||
| const out = new Map<string, Set<string>>() | ||
| try { | ||
| const result = | ||
| tables === undefined | ||
| ? await client.query<{ table_name: string; column_name: string }>( | ||
| `SELECT table_name, column_name FROM information_schema.columns | ||
| WHERE table_schema = current_schema()`, | ||
| ) | ||
| : await client.query<{ table_name: string; column_name: string }>( | ||
| `SELECT table_name, column_name FROM information_schema.columns | ||
| WHERE table_schema = current_schema() | ||
| AND table_name = ANY($1::text[])`, | ||
| [tables], | ||
| ) | ||
| for (const row of result.rows) { | ||
| const set = out.get(row.table_name) ?? new Set<string>() | ||
| set.add(row.column_name) | ||
| out.set(row.table_name, set) | ||
| } | ||
| } catch { | ||
| // information_schema is always present; failures here are surprising | ||
| // enough to swallow rather than crash the read-only status path. | ||
| } | ||
| return out | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add an E2E test for the new CLI flag wiring
Line 399 and Lines 405-409 change top-level CLI dispatch behavior; this needs an accompanying E2E test to lock argv/command behavior and exit semantics.
As per coding guidelines,
packages/cli/src/bin/stash.ts: “Add an E2E test when touchingsrc/bin/stash.tsargv parsing, exit codes, or top-level error handling.”🤖 Prompt for AI Agents