Skip to content
Merged

Next #100

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/binary-no-dotenv-autoload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@noormdev/cli': patch
---

Stop the `noorm` binary from loading `.env` in the working directory. Bun's loader expanded `$` and cut values at `#`, so a `NOORM_CONNECTION_PASSWORD` containing those characters reached the database as a different password and login failed. `NOORM_*` variables now come only from the process environment: export them in the shell or set them in CI.
6 changes: 6 additions & 0 deletions .changeset/connection-error-reasons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@noormdev/cli': patch
'@noormdev/sdk': patch
---

Say why a database connection failed: refused port, unknown host, timeout, rejected TLS certificate, disabled, locked, or expired account, missing grant, connection limit, missing password, and SQLite file or directory faults. Where the server withholds the reason (SQL Server 18456, PostgreSQL 28P01, MySQL 1045), the message says so and lists the usual causes. Exhausted retries report the server's last error. `connection:error` log entries carry `serverCode` and `serverMessage`.
6 changes: 6 additions & 0 deletions .changeset/mssql-unprivileged-logins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@noormdev/cli': patch
'@noormdev/sdk': patch
---

Let MSSQL logins without server-level access connect. Connecting no longer detours through `master` to look the target up in `sys.databases`, so contained database users (the usual account on Azure SQL Database) and logins without `VIEW ANY DATABASE` can connect, and the config add/edit connection test passes for them. A database that is missing, or that the login cannot open, now fails with that reason instead of "Login failed".
6 changes: 6 additions & 0 deletions .changeset/slow-statement-watcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@noormdev/cli': minor
'@noormdev/sdk': minor
---

Show what a long-running SQL file is doing. After 10 seconds the runner asks the server about the file's session every 10 seconds, from a second pooled connection, and emits `file:progress`: elapsed time, the command's own progress (PostgreSQL `pg_stat_progress_*`, SQL Server `percent_complete`, MySQL stage counters), and the sessions it is waiting on. The TUI run and change screens render it under the running file, and the log records it. Build and exec runs in the TUI cancel on a second `Escape` within 2 seconds; PostgreSQL and MySQL stop the running statement on the server.
6 changes: 6 additions & 0 deletions .changeset/tui-explore-duplicate-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@noormdev/cli': patch
'@noormdev/sdk': patch
---

Stop TUI lists from leaving stale rows on screen when two rows share an identity: explore indexes and foreign keys whose names repeat across tables (SQL Server's `IX_UserId`, MySQL's `PRIMARY`), PostgreSQL function and procedure overloads, and settings rules with the same description. `listFunctions` and `listProcedures` now return a `signature` on PostgreSQL that tells overloads apart.
5 changes: 5 additions & 0 deletions .changeset/tui-typing-blocks-shortcuts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@noormdev/cli': patch
---

Stop global TUI shortcuts from firing while you type. A capital `L` or `Q` typed into a form field, search box, or the SQL editor used to open the log viewer or the SQL terminal, and `?`, `D`, and `F` could open help or toggle dry-run and force mode. While a text field is taking input, those keys now type their character.
25 changes: 25 additions & 0 deletions .claude/rules/wiki/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/cli/*.ts"
- "src/cli/ci/**"
- "src/cli/config/**"
- "src/cli/dev/**"
- "src/cli/lock/**"
- "src/cli/settings/**"
- "packages/cli/**"
- "skills/noorm/**"
- "tests/cli/*"
- "tests/integration/cli/**"
---

Domain: cli. Citty CLI command groups and leaf commands, headless mode, binary distribution.

Map:
- docs/wiki/cli.md
Contracts:
- docs/spec/v1-49-54-cli-field-defects.md
Designs:
- docs/design/v1-49-54-cli-field-defects.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
17 changes: 17 additions & 0 deletions .claude/rules/wiki/core-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/change/**"
- "src/cli/change/**"
- "tests/core/change/**"
- "tests/integration/change/**"
---

Domain: core-change. Versioned DB changes: scaffold, parse, execute, revert, history (timestamps hydrated as UTC on pg/mysql).

Map:
- docs/wiki/core-change.md
Contracts:
- docs/spec/v1-45-rewind-tiebreak.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
28 changes: 28 additions & 0 deletions .claude/rules/wiki/core-db.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/db/**"
- "src/core/connection/**"
- "src/core/explore/**"
- "src/core/teardown/**"
- "src/core/transfer/**"
- "src/cli/db/**"
- "tests/core/connection/**"
- "tests/core/db/**"
- "tests/core/explore/**"
- "tests/core/teardown/**"
- "tests/core/transfer/**"
- "tests/integration/connection/**"
- "tests/integration/error-diagnostics/**"
- "tests/integration/explore/**"
- "tests/integration/impersonate/**"
- "tests/integration/teardown/**"
- "tests/integration/transfer/**"
---

Domain: core-db. DB lifecycle: create/drop, connections and session helpers, explore schema, teardown, cross-DB transfer.

Map:
- docs/wiki/core-db.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
24 changes: 24 additions & 0 deletions .claude/rules/wiki/core-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/identity/**"
- "src/core/vault/**"
- "src/core/logger/**"
- "src/core/sql-terminal/**"
- "src/cli/identity/**"
- "src/cli/secret/**"
- "src/cli/vault/**"
- "src/cli/sql/**"
- "tests/core/identity/**"
- "tests/core/vault/**"
- "tests/core/logger/**"
- "tests/core/sql-terminal/**"
- "tests/integration/sql-terminal/**"
---

Domain: core-identity. Identity keypairs, vault secrets, structured logger, SQL terminal history.

Map:
- docs/wiki/core-identity.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
17 changes: 17 additions & 0 deletions .claude/rules/wiki/core-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/policy/**"
- "tests/core/policy/**"
---

Domain: core-policy. Access-control policy: role×permission matrix, SQL statement classifier, legacy `protected`→`access` migration.

Map:
- docs/wiki/core-policy.md
Contracts:
- docs/spec/config-access-roles.md
Designs:
- docs/design/config-access-roles.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
17 changes: 17 additions & 0 deletions .claude/rules/wiki/core-runner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/runner/**"
- "src/core/template/**"
- "src/cli/run/**"
- "tests/core/runner/**"
- "tests/core/template/**"
- "tests/integration/runner/**"
---

Domain: core-runner. SQL file execution with checksum dedup, Eta templating, and long-running statement progress and cancel.

Map:
- docs/wiki/core-runner.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
30 changes: 30 additions & 0 deletions .claude/rules/wiki/core-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/state/**"
- "src/core/settings/**"
- "src/core/config/**"
- "src/core/lifecycle/**"
- "src/core/version/**"
- "src/core/project.ts"
- "src/core/project-init.ts"
- "src/core/environment.ts"
- "src/core/observer.ts"
- "tests/core/state/**"
- "tests/core/settings/**"
- "tests/core/config/**"
- "tests/core/lifecycle/**"
- "tests/core/version/**"
- "tests/integration/version/**"
---

Domain: core-state. Encrypted state, settings.yml, config resolution, lifecycle, version migration, observer events.

Map:
- docs/wiki/core-state.md
Contracts:
- docs/spec/config-access-roles.md
Designs:
- docs/design/config-access-roles.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
18 changes: 18 additions & 0 deletions .claude/rules/wiki/infra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- ".github/**"
- "scripts/**"
- "examples/**"
- "docs/**"
- "tsup.*.config.ts"
- "docker-compose.test.yml"
- "bunfig.toml"
---

Domain: infra. CI, build pipeline, binary release, example projects, VitePress docs.

Map:
- docs/wiki/infra.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
16 changes: 16 additions & 0 deletions .claude/rules/wiki/mcp-rpc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/mcp/**"
- "src/rpc/**"
- "src/cli/mcp/**"
- "tests/core/mcp/**"
- "tests/core/rpc/**"
---

Domain: mcp-rpc. MCP server over stdio wrapping flat RPC command registry, permission-gated dispatch.

Map:
- docs/wiki/mcp-rpc.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
22 changes: 22 additions & 0 deletions .claude/rules/wiki/sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/sdk/**"
- "src/core/dt/**"
- "packages/sdk/**"
- "tests/sdk/**"
- "tests/integration/sdk/**"
---

Domain: sdk. Programmatic API (`createContext`) + DT binary serialization format.

Map:
- docs/wiki/sdk.md
Contracts:
- docs/spec/sdk-with-schema.md
References:
- docs/reference/sdk.md
Designs:
- docs/design/sdk-with-schema.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
20 changes: 20 additions & 0 deletions .claude/rules/wiki/tui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/tui/**"
- ".claude/rules/tui-development.md"
- "tests/cli/components/**"
- "tests/cli/hooks/**"
- "tests/cli/screens/**"
---

Domain: tui. Ink/React TUI with focus manager, keyboard routing, registered screens.

Map:
- docs/wiki/tui.md
Contracts:
- docs/spec/update-download-progress.md
Designs:
- docs/design/update-download-progress.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
15 changes: 15 additions & 0 deletions .claude/rules/wiki/worker-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
# generated by /refresh-wiki — do not hand-edit; regenerated every refresh
paths:
- "src/core/worker-bridge/**"
- "src/workers/**"
- "tests/core/worker-bridge/**"
- "tests/workers/**"
---

Domain: worker-bridge. Hub-and-spoke worker threads for DT serialization and DB connection worker.

Map:
- docs/wiki/worker-bridge.md

Consult the map before changing behavior here. Behavior changes stale the pages above. Renames or removals stale mentions beyond them: grep the old name across docs/ before shipping.
71 changes: 71 additions & 0 deletions docs/dev/runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ An empty file (or one that contains only comments after stripping `GO`s) is trea
| `file:before` | `{ filepath, checksum, configName }` | About to execute a file |
| `file:after` | `{ filepath, status, durationMs, error? }` | File execution completed |
| `file:skip` | `{ filepath, reason }` | File skipped; `reason` is `'unchanged'` or `'already-run'` |
| `file:progress` | `{ filepath, elapsedMs, sessionId, status }` | File still running past the watch delay; repeats every watch interval. See [Long-Running Statements](#long-running-statements) |
| `file:dry-run` | `{ filepath, status, outputPath?, error? }` | File rendered to `tmp/` (or failed to render) |

```typescript
Expand All @@ -301,6 +302,75 @@ observer.on('build:complete', ({ filesRun, filesSkipped, durationMs }) => {
```


## Long-Running Statements

A file runs as one call, so from the client a 20-minute `CREATE INDEX` and a file queued behind another session's lock look the same. `StatementWatcher` ([`src/core/runner/statement-watcher.ts`](../../src/core/runner/statement-watcher.ts)) tells them apart by asking the server. The runner routes every file's SQL through it with one watcher per run; the change executor uses one watcher per change.

A file that finishes inside 10 seconds costs one extra query (its session id) and nothing else. A file that runs longer gets a report every 10 seconds until it ends:

```mermaid
sequenceDiagram
participant R as runner
participant W as StatementWatcher
participant P as pinned connection
participant S as side connection
R->>W: run(filepath, db, fn)
W->>P: SESSION_ID_SQL
W->>P: fn(conn) runs the file
Note over W: 10s delay passes
W->>S: checkout (first slow file only)
loop every 10s until the file ends
W->>S: STATEMENT_PROBES[dialect](sessionId)
W-->>R: emit file:progress
end
P-->>W: file done, timer cleared
R->>W: close() at end of run returns S to the pool
```

The file is pinned with `db.connection()` so the session id read first is the session the SQL runs on. A transaction executor (postgres changes) is already one connection and runs as is. The side connection comes from the same pool, is checked out only when a file first runs long, and is held until `close()` so later slow files reuse it.

`status` in the event is what the server reported:

| Dialect | Source | Reports |
|---------|--------|---------|
| postgres | `pg_stat_activity`, `pg_blocking_pids()`, `pg_stat_progress_{create_index,vacuum,cluster,copy,analyze}` | state, wait event, blockers with query and age, parallel workers, phase and done/total |
| mssql | `sys.dm_exec_requests` | status, wait type, blocking session, `percent_complete` |
| mysql | `information_schema.processlist`, `sys.innodb_lock_waits`, `sys.schema_table_lock_waits`, `performance_schema.events_stages_current` | thread state, blockers, stage done/estimated |
| sqlite | none | `status` is null; elapsed time only |

Each part of a probe is attempted on its own. A missing privilege (`VIEW SERVER STATE` on mssql, the `sys` schema on mysql) drops that part of the report, not the report.

What bites:

| Condition | Effect |
|-----------|--------|
| Pool has no spare connection (`connection.pool.max: 1`) | The side checkout waits 5 seconds, then the watcher gives it up for the rest of the run: reports carry elapsed time only and cancel stops only between files. A checkout that arrives after its file ended, or after the watcher gave up, is returned to the pool at once |
| Transaction-mode pooler (PgBouncer `pool_mode = transaction`, RDS Proxy, Supabase port 6543) | The session-id read and the file can land on different backends. Reports can describe another client's session and a cancel can stop another client's statement. Point noorm at the database directly, or at a session-mode pooler |
| Postgres change on a transaction | The file already runs on the change's transaction, so no second pin happens; the side connection is still a separate pooled session |

### Cancellation

Aborting `RunContext.signal` stops the run. The runner starts no further file, marks the remaining ones skipped, and returns `error: 'Run cancelled'`. What happens to the file already running depends on the dialect:

| Dialect | Running file |
|---------|--------------|
| postgres | `pg_cancel_backend(pid)` from the side connection; the file fails and its implicit transaction rolls back |
| mysql | `KILL QUERY id` from the side connection |
| mssql | Runs to completion. Kysely's `MssqlDialect` never exposes tedious's `Request`, and `KILL` would end the session, not the request |
| sqlite | Runs to completion. It is in-process on one connection, so there is no second session to send a cancel from |

A file the signal aborted before its SQL was sent does not start, and is skipped with the rest. The pinned connection goes back to the pool only after an in-flight cancel returns, so a cancel that lands late cannot hit the next query on that session. A last file that completes despite the abort leaves the run successful, because nothing was cut short.

The session-id queries and the cancel table live in [`src/core/connection/session.ts`](../../src/core/connection/session.ts), shared with the SQL terminal's cancel.

```typescript
const controller = new AbortController();

const result = await runBuild({ ...context, signal: controller.signal }, sqlPath);
// elsewhere: controller.abort();
```


## Additional Utilities

The runner module exports several utility functions:
Expand Down Expand Up @@ -520,6 +590,7 @@ interface RunContext {
config?: Record<string, unknown> // Config object for template context
secrets?: Record<string, string> // Config-scoped secrets
globalSecrets?: Record<string, string> // Global secrets from state
signal?: AbortSignal // abort to cancel the run
}
```

Expand Down
Loading
Loading