@chaprola/mcp-server 1.6.3 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +44 -28
- package/package.json +2 -2
- package/references/ref-gotchas.md +1 -0
- package/references/ref-import.md +12 -0
- package/references/ref-schema.md +166 -0
package/dist/index.js
CHANGED
|
@@ -102,7 +102,7 @@ const server = new McpServer({
|
|
|
102
102
|
- **Run programs:** chaprola_run (single execution), chaprola_run_each (per-record batch), chaprola_report (published reports)
|
|
103
103
|
- **Email:** chaprola_email_send, chaprola_email_inbox, chaprola_email_read
|
|
104
104
|
- **Web:** chaprola_search (Brave API), chaprola_fetch (URL → markdown)
|
|
105
|
-
- **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields)
|
|
105
|
+
- **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields — NON-DESTRUCTIVE. Use this, NOT chaprola_import, to modify schemas on existing data.)
|
|
106
106
|
- **Export:** chaprola_export (JSON or FHIR — full round-trip: FHIR in, process, FHIR out)
|
|
107
107
|
- **Schedule:** chaprola_schedule (cron jobs for any endpoint)
|
|
108
108
|
|
|
@@ -174,6 +174,9 @@ server.resource("ref-import", "chaprola://ref/import", { description: "Import, e
|
|
|
174
174
|
server.resource("ref-query", "chaprola://ref/query", { description: "Query, sort, index, merge, record CRUD — data operations", mimeType: "text/markdown" }, async () => ({
|
|
175
175
|
contents: [{ uri: "chaprola://ref/query", mimeType: "text/markdown", text: readRef("ref-query.md") }],
|
|
176
176
|
}));
|
|
177
|
+
server.resource("ref-schema", "chaprola://ref/schema", { description: "Schema operations — /format (inspect), /alter (widen/rename/add/drop fields). CRITICAL: Use /alter for schema changes, NOT /import.", mimeType: "text/markdown" }, async () => ({
|
|
178
|
+
contents: [{ uri: "chaprola://ref/schema", mimeType: "text/markdown", text: readRef("ref-schema.md") }],
|
|
179
|
+
}));
|
|
177
180
|
server.resource("ref-pivot", "chaprola://ref/pivot", { description: "Pivot tables (GROUP BY) — row, column, aggregate functions", mimeType: "text/markdown" }, async () => ({
|
|
178
181
|
contents: [{ uri: "chaprola://ref/pivot", mimeType: "text/markdown", text: readRef("ref-pivot.md") }],
|
|
179
182
|
}));
|
|
@@ -277,8 +280,9 @@ server.tool("chaprola_report", "Run a published program and return output. No au
|
|
|
277
280
|
project: z.string().describe("Project containing the program"),
|
|
278
281
|
name: z.string().describe("Name of the published .PR file"),
|
|
279
282
|
token: z.string().optional().describe("Action token (act_...) for writable reports. Required to persist WRITE/DELETE/QUERY operations. Provided when program was published with writable=true."),
|
|
280
|
-
params: z.
|
|
281
|
-
}, async ({ userid, project, name, token, params }) => {
|
|
283
|
+
params: z.string().optional().describe("JSON object of named params, e.g. {\"deck\": \"kanji\", \"level\": 3}. Named params are read in programs via PARAM.name. Legacy R-variables (r1-r20) also supported. Use chaprola_report_params to discover what params a report accepts."),
|
|
284
|
+
}, async ({ userid, project, name, token, params: paramsStr }) => {
|
|
285
|
+
const params = typeof paramsStr === 'string' ? JSON.parse(paramsStr) : paramsStr;
|
|
282
286
|
const urlParams = new URLSearchParams();
|
|
283
287
|
urlParams.set("userid", userid);
|
|
284
288
|
urlParams.set("project", project);
|
|
@@ -330,13 +334,14 @@ server.tool("chaprola_baa_status", "Check whether the authenticated user has sig
|
|
|
330
334
|
return textResult(res);
|
|
331
335
|
});
|
|
332
336
|
// --- Import ---
|
|
333
|
-
server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA). Sign BAA first if handling PHI", {
|
|
337
|
+
server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA). DESTRUCTIVE: This REPLACES both the format (.F) and data (.DA) files if they already exist. All existing data will be lost. Use chaprola_alter to modify field widths/schema on existing data. Use chaprola_import only for new data or when replacing entire datasets. Sign BAA first if handling PHI", {
|
|
334
338
|
project: z.string().describe("Project name"),
|
|
335
339
|
name: z.string().describe("File name (without extension)"),
|
|
336
|
-
data: z.
|
|
340
|
+
data: z.string().describe("JSON array of record objects to import"),
|
|
337
341
|
format: z.enum(["json", "fhir"]).optional().describe("Data format: json (default) or fhir"),
|
|
338
342
|
expires_in_days: z.number().optional().describe("Days until data expires (default: 90)"),
|
|
339
|
-
}, async ({ project, name, data, format, expires_in_days }) => withBaaCheck(async () => {
|
|
343
|
+
}, async ({ project, name, data: dataStr, format, expires_in_days }) => withBaaCheck(async () => {
|
|
344
|
+
const data = typeof dataStr === 'string' ? JSON.parse(dataStr) : dataStr;
|
|
340
345
|
const { username } = getCredentials();
|
|
341
346
|
const body = { userid: username, project, name, data };
|
|
342
347
|
if (format)
|
|
@@ -346,7 +351,7 @@ server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F
|
|
|
346
351
|
const res = await authedFetch("/import", body);
|
|
347
352
|
return textResult(res);
|
|
348
353
|
}));
|
|
349
|
-
server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit)", {
|
|
354
|
+
server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit). DESTRUCTIVE: The subsequent chaprola_import_process will replace existing data. Use chaprola_alter to modify schemas on existing data.", {
|
|
350
355
|
project: z.string().describe("Project name"),
|
|
351
356
|
name: z.string().describe("File name (without extension)"),
|
|
352
357
|
}, async ({ project, name }) => withBaaCheck(async () => {
|
|
@@ -354,7 +359,7 @@ server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large file
|
|
|
354
359
|
const res = await authedFetch("/import-url", { userid: username, project, name });
|
|
355
360
|
return textResult(res);
|
|
356
361
|
}));
|
|
357
|
-
server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files", {
|
|
362
|
+
server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files. DESTRUCTIVE: Replaces existing data if the file already exists. Use chaprola_alter to modify schemas on existing data.", {
|
|
358
363
|
project: z.string().describe("Project name"),
|
|
359
364
|
name: z.string().describe("File name (without extension)"),
|
|
360
365
|
format: z.enum(["json", "fhir"]).optional().describe("Data format: json (default) or fhir"),
|
|
@@ -366,10 +371,10 @@ server.tool("chaprola_import_process", "Process a file previously uploaded to S3
|
|
|
366
371
|
const res = await authedFetch("/import-process", body);
|
|
367
372
|
return textResult(res);
|
|
368
373
|
}));
|
|
369
|
-
server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference", {
|
|
374
|
+
server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference. DESTRUCTIVE: Replaces existing data if the file already exists. Use chaprola_alter to modify schemas on existing data.", {
|
|
370
375
|
project: z.string().describe("Project name"),
|
|
371
376
|
name: z.string().describe("Output file name (without extension)"),
|
|
372
|
-
url: z.string().
|
|
377
|
+
url: z.string().describe("Public URL to download (http/https only)"),
|
|
373
378
|
instructions: z.string().optional().describe("Natural language instructions for AI-powered field selection and transforms"),
|
|
374
379
|
max_rows: z.number().optional().describe("Maximum rows to import (default: 5,000,000)"),
|
|
375
380
|
}, async ({ project, name, url, instructions, max_rows }) => withBaaCheck(async () => {
|
|
@@ -539,16 +544,22 @@ server.tool("chaprola_download", "Get a presigned S3 URL to download any file yo
|
|
|
539
544
|
server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregation, ORDER BY, JOIN, pivot, and Mercury scoring", {
|
|
540
545
|
project: z.string().describe("Project name"),
|
|
541
546
|
file: z.string().describe("Data file to query"),
|
|
542
|
-
where: z.
|
|
547
|
+
where: z.string().optional().describe("JSON object of filter conditions, e.g. {\"field\": \"status\", \"op\": \"eq\", \"value\": \"active\"}. Ops: eq, ne, gt, ge, lt, le, between, contains, starts_with"),
|
|
543
548
|
select: z.array(z.string()).optional().describe("Fields to include in output"),
|
|
544
|
-
aggregate: z.
|
|
545
|
-
order_by: z.
|
|
549
|
+
aggregate: z.string().optional().describe("JSON array of aggregation specs, e.g. [{\"field\": \"amount\", \"func\": \"sum\"}]. Funcs: count, sum, avg, min, max, stddev"),
|
|
550
|
+
order_by: z.string().optional().describe("JSON array of sort specs, e.g. [{\"field\": \"name\", \"dir\": \"asc\"}]"),
|
|
546
551
|
limit: z.number().optional().describe("Max results to return"),
|
|
547
552
|
offset: z.number().optional().describe("Skip this many results"),
|
|
548
|
-
join: z.
|
|
549
|
-
pivot: z.
|
|
550
|
-
mercury: z.
|
|
551
|
-
}, async ({ project, file, where, select, aggregate, order_by, limit, offset, join, pivot, mercury }) => withBaaCheck(async () => {
|
|
553
|
+
join: z.string().optional().describe("JSON object of join config, e.g. {\"file\": \"other\", \"on\": \"id\", \"type\": \"inner\"}"),
|
|
554
|
+
pivot: z.string().optional().describe("JSON object of pivot config, e.g. {\"row\": \"category\", \"column\": \"month\", \"values\": \"sales\"}"),
|
|
555
|
+
mercury: z.string().optional().describe("JSON object of mercury scoring config, e.g. {\"fields\": [{\"field\": \"score\", \"target\": 100, \"weight\": 1.0}]}"),
|
|
556
|
+
}, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr }) => withBaaCheck(async () => {
|
|
557
|
+
const where = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
558
|
+
const aggregate = typeof aggregateStr === 'string' ? JSON.parse(aggregateStr) : aggregateStr;
|
|
559
|
+
const order_by = typeof orderByStr === 'string' ? JSON.parse(orderByStr) : orderByStr;
|
|
560
|
+
const join = typeof joinStr === 'string' ? JSON.parse(joinStr) : joinStr;
|
|
561
|
+
const pivot = typeof pivotStr === 'string' ? JSON.parse(pivotStr) : pivotStr;
|
|
562
|
+
const mercury = typeof mercuryStr === 'string' ? JSON.parse(mercuryStr) : mercuryStr;
|
|
552
563
|
const { username } = getCredentials();
|
|
553
564
|
const body = { userid: username, project, file };
|
|
554
565
|
if (where)
|
|
@@ -617,7 +628,7 @@ server.tool("chaprola_format", "Inspect a data file's schema — returns field n
|
|
|
617
628
|
const res = await authedFetch("/format", { userid: username, project, name });
|
|
618
629
|
return textResult(res);
|
|
619
630
|
}));
|
|
620
|
-
server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename fields, add new fields, drop fields. Transforms existing data to match the new schema.", {
|
|
631
|
+
server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename fields, add new fields, drop fields. NON-DESTRUCTIVE: Transforms existing data to match the new schema. This is the ONLY safe way to change field widths or schema on existing data files. Unlike chaprola_import which replaces all data, chaprola_alter preserves and reformats existing records.", {
|
|
621
632
|
project: z.string().describe("Project name"),
|
|
622
633
|
name: z.string().describe("Data file name (without extension)"),
|
|
623
634
|
alter: z.array(z.object({
|
|
@@ -756,7 +767,7 @@ server.tool("chaprola_search", "Search the web via Brave Search API. Returns tit
|
|
|
756
767
|
}));
|
|
757
768
|
// --- Fetch ---
|
|
758
769
|
server.tool("chaprola_fetch", "Fetch any URL and return clean content. HTML pages converted to markdown. SSRF-protected. Rate limit: 20/day per user", {
|
|
759
|
-
url: z.string().
|
|
770
|
+
url: z.string().describe("URL to fetch (http:// or https://)"),
|
|
760
771
|
format: z.enum(["markdown", "text", "html", "json"]).optional().describe("Output format (default: markdown)"),
|
|
761
772
|
max_length: z.number().optional().describe("Max output characters (default: 50000, max: 200000)"),
|
|
762
773
|
}, async ({ url, format, max_length }) => withBaaCheck(async () => {
|
|
@@ -773,9 +784,10 @@ server.tool("chaprola_schedule", "Create a scheduled job that runs a Chaprola en
|
|
|
773
784
|
name: z.string().describe("Unique name for this schedule (alphanumeric + hyphens/underscores)"),
|
|
774
785
|
cron: z.string().describe("Standard 5-field cron expression (min hour day month weekday). Minimum interval: 15 minutes"),
|
|
775
786
|
endpoint: z.enum(["/import-download", "/run", "/export-report", "/search", "/fetch", "/query", "/email/send", "/export", "/report", "/list"]).describe("Target endpoint to call"),
|
|
776
|
-
body: z.
|
|
787
|
+
body: z.string().describe("JSON object of the schedule request body. userid is injected automatically"),
|
|
777
788
|
skip_if_unchanged: z.boolean().optional().describe("Skip when response matches previous run (SHA-256 hash). Default: false"),
|
|
778
|
-
}, async ({ name, cron, endpoint, body, skip_if_unchanged }) => withBaaCheck(async () => {
|
|
789
|
+
}, async ({ name, cron, endpoint, body: bodyStr, skip_if_unchanged }) => withBaaCheck(async () => {
|
|
790
|
+
const body = typeof bodyStr === 'string' ? JSON.parse(bodyStr) : bodyStr;
|
|
779
791
|
const reqBody = { name, cron, endpoint, body };
|
|
780
792
|
if (skip_if_unchanged !== undefined)
|
|
781
793
|
reqBody.skip_if_unchanged = skip_if_unchanged;
|
|
@@ -796,8 +808,9 @@ server.tool("chaprola_schedule_delete", "Delete a scheduled job by name", {
|
|
|
796
808
|
server.tool("chaprola_insert_record", "Insert a new record into a data file's merge file (.MRG). The record appears at the end of the file until consolidation.", {
|
|
797
809
|
project: z.string().describe("Project name"),
|
|
798
810
|
file: z.string().describe("Data file name (without extension)"),
|
|
799
|
-
record: z.
|
|
800
|
-
}, async ({ project, file, record }) => withBaaCheck(async () => {
|
|
811
|
+
record: z.string().describe("JSON object of the record to insert, e.g. {\"name\": \"foo\", \"status\": \"active\"}. Unspecified fields default to blanks."),
|
|
812
|
+
}, async ({ project, file, record: recordStr }) => withBaaCheck(async () => {
|
|
813
|
+
const record = typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr;
|
|
801
814
|
const { username } = getCredentials();
|
|
802
815
|
const res = await authedFetch("/insert-record", { userid: username, project, file, record });
|
|
803
816
|
return textResult(res);
|
|
@@ -805,9 +818,11 @@ server.tool("chaprola_insert_record", "Insert a new record into a data file's me
|
|
|
805
818
|
server.tool("chaprola_update_record", "Update fields in a single record matched by a where clause. If no sort-key changes, updates in place; otherwise marks old record ignored and appends to merge file.", {
|
|
806
819
|
project: z.string().describe("Project name"),
|
|
807
820
|
file: z.string().describe("Data file name (without extension)"),
|
|
808
|
-
where: z.
|
|
809
|
-
set: z.
|
|
810
|
-
}, async ({ project, file, where:
|
|
821
|
+
where: z.string().describe("JSON object of filter conditions for which records to update, e.g. {\"id\": \"123\"}"),
|
|
822
|
+
set: z.string().describe("JSON object of fields to update, e.g. {\"status\": \"done\"}"),
|
|
823
|
+
}, async ({ project, file, where: whereStr, set: setStr }) => withBaaCheck(async () => {
|
|
824
|
+
const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
825
|
+
const set = typeof setStr === 'string' ? JSON.parse(setStr) : setStr;
|
|
811
826
|
const { username } = getCredentials();
|
|
812
827
|
const res = await authedFetch("/update-record", { userid: username, project, file, where: whereClause, set });
|
|
813
828
|
return textResult(res);
|
|
@@ -815,8 +830,9 @@ server.tool("chaprola_update_record", "Update fields in a single record matched
|
|
|
815
830
|
server.tool("chaprola_delete_record", "Delete a single record matched by a where clause. Marks the record as ignored (.IGN). Physically removed on consolidation.", {
|
|
816
831
|
project: z.string().describe("Project name"),
|
|
817
832
|
file: z.string().describe("Data file name (without extension)"),
|
|
818
|
-
where: z.
|
|
819
|
-
}, async ({ project, file, where:
|
|
833
|
+
where: z.string().describe("JSON object of filter conditions for which records to delete, e.g. {\"id\": \"123\"}"),
|
|
834
|
+
}, async ({ project, file, where: whereStr }) => withBaaCheck(async () => {
|
|
835
|
+
const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
820
836
|
const { username } = getCredentials();
|
|
821
837
|
const res = await authedFetch("/delete-record", { userid: username, project, file, where: whereClause });
|
|
822
838
|
return textResult(res);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaprola/mcp-server",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "MCP server for Chaprola — agent-first data platform. Gives AI agents
|
|
3
|
+
"version": "1.6.4",
|
|
4
|
+
"description": "MCP server for Chaprola — agent-first data platform. Gives AI agents 47 tools for structured data storage, record CRUD, querying, schema inspection, web search, URL fetching, scheduled jobs, and execution via plain HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- **Statement numbers are labels, not line numbers.** Only number GOTO/CALL targets.
|
|
12
12
|
|
|
13
13
|
## API
|
|
14
|
+
- **NEVER use `/import` to change field widths.** `/import` REPLACES existing data. Use `/alter` to widen/narrow fields while preserving data. See `chaprola://ref/schema`.
|
|
14
15
|
- **userid must match authenticated user.** 403 on mismatch.
|
|
15
16
|
- **Login invalidates the old key.** Save the new one immediately.
|
|
16
17
|
- **Async for large datasets.** `/run` with `async: true` for >100K records (API Gateway 30s timeout).
|
package/references/ref-import.md
CHANGED
|
@@ -10,6 +10,14 @@ POST /import {userid, project, name: "STAFF", data: [{"name": "Alice", "salary":
|
|
|
10
10
|
|
|
11
11
|
Field widths auto-sized from longest value. Default expiry: 90 days. Override with `expires_in_days`.
|
|
12
12
|
|
|
13
|
+
### ⚠️ DESTRUCTIVE WARNING
|
|
14
|
+
|
|
15
|
+
**`/import` REPLACES both the format (.F) and data (.DA) files if they already exist. All existing data will be lost.**
|
|
16
|
+
|
|
17
|
+
- **Use `/import` ONLY for:** Creating brand new data files or intentionally replacing entire datasets
|
|
18
|
+
- **DO NOT use `/import` to:** Change field widths or modify schema on existing data
|
|
19
|
+
- **Use `/alter` instead** to modify field widths/schema while preserving existing data (see `chaprola://ref/schema`)
|
|
20
|
+
|
|
13
21
|
## Large File Upload (presigned URL)
|
|
14
22
|
```bash
|
|
15
23
|
POST /import-url {userid, project, name} → {upload_url, staging_key}
|
|
@@ -17,11 +25,15 @@ POST /import-url {userid, project, name} → {upload_url, staging_key}
|
|
|
17
25
|
POST /import-process {userid, project, name, staging_key} → same as /import
|
|
18
26
|
```
|
|
19
27
|
|
|
28
|
+
**WARNING:** `/import-process` is also DESTRUCTIVE and replaces existing data. Use `/alter` for schema changes.
|
|
29
|
+
|
|
20
30
|
## POST /import-download
|
|
21
31
|
`{userid, project, name, url, instructions?, max_rows?}`
|
|
22
32
|
Imports directly from URL. Supports: CSV, TSV, JSON, NDJSON, Parquet, Excel (.xlsx/.xls).
|
|
23
33
|
Optional `instructions` for AI schema inference. Max 1M records.
|
|
24
34
|
|
|
35
|
+
**WARNING:** `/import-download` is also DESTRUCTIVE and replaces existing data. Use `/alter` for schema changes.
|
|
36
|
+
|
|
25
37
|
## POST /export
|
|
26
38
|
`{userid, project, name, format?}` → `{data: [...records]}`
|
|
27
39
|
Optional `format: "fhir"` for FHIR JSON reconstruction.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Schema Inspection & Modification
|
|
2
|
+
|
|
3
|
+
## POST /format
|
|
4
|
+
Inspect a data file's schema — returns field names, positions, lengths, types, and PHI flags.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
POST /format {userid, project, name: "STAFF"}
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"format_file": "s3://chaprola-2026/userid/project/format/STAFF.F",
|
|
14
|
+
"fields": [
|
|
15
|
+
{"name": "name", "position": 1, "length": 50, "type": "text", "phi": false},
|
|
16
|
+
{"name": "salary", "position": 51, "length": 10, "type": "numeric", "phi": false}
|
|
17
|
+
],
|
|
18
|
+
"record_length": 60
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use this to inspect the current schema before making changes with `/alter`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## POST /alter
|
|
27
|
+
**NON-DESTRUCTIVE schema modification.** Modifies field widths, renames fields, adds new fields, or drops fields. Existing data is preserved and reformatted to match the new schema.
|
|
28
|
+
|
|
29
|
+
### ⚠️ CRITICAL: Use /alter for schema changes, NOT /import
|
|
30
|
+
|
|
31
|
+
**DO NOT use `/import` to change field widths or schema on existing data files.** `/import` REPLACES both the format (.F) and data (.DA) files, destroying all existing data.
|
|
32
|
+
|
|
33
|
+
**Use `/alter` when you need to:**
|
|
34
|
+
- Widen or narrow field widths
|
|
35
|
+
- Rename fields
|
|
36
|
+
- Add new fields to existing data
|
|
37
|
+
- Drop unused fields
|
|
38
|
+
- Change field types
|
|
39
|
+
|
|
40
|
+
**Use `/import` only when:**
|
|
41
|
+
- Creating a brand new data file
|
|
42
|
+
- Intentionally replacing an entire dataset
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## /alter Request Format
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"userid": "...",
|
|
51
|
+
"project": "...",
|
|
52
|
+
"name": "STAFF",
|
|
53
|
+
"alter": [
|
|
54
|
+
{"field": "name", "width": 100}, // widen from 50 to 100
|
|
55
|
+
{"field": "dept", "rename": "department"}
|
|
56
|
+
],
|
|
57
|
+
"add": [
|
|
58
|
+
{"name": "email", "width": 80, "type": "text", "after": "name"}
|
|
59
|
+
],
|
|
60
|
+
"drop": ["old_field"],
|
|
61
|
+
"output": "STAFF_V2" // optional: create new file instead of in-place
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Parameters
|
|
66
|
+
|
|
67
|
+
**`alter`** (optional): Array of field modifications
|
|
68
|
+
- `field` (required): Field name to modify
|
|
69
|
+
- `width` (optional): New width (can widen or narrow)
|
|
70
|
+
- `rename` (optional): New field name
|
|
71
|
+
- `type` (optional): Change type (`"text"` or `"numeric"`)
|
|
72
|
+
|
|
73
|
+
**`add`** (optional): Array of new fields to add
|
|
74
|
+
- `name` (required): New field name
|
|
75
|
+
- `width` (required): Field width
|
|
76
|
+
- `type` (optional): `"text"` (default) or `"numeric"`
|
|
77
|
+
- `after` (optional): Insert after this field (default: end of record)
|
|
78
|
+
|
|
79
|
+
**`drop`** (optional): Array of field names to remove
|
|
80
|
+
|
|
81
|
+
**`output`** (optional): Output file name. If not specified, modifies in-place.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Widen a field (most common case)
|
|
88
|
+
```bash
|
|
89
|
+
# Widen 'description' field from 100 to 500 characters
|
|
90
|
+
POST /alter {
|
|
91
|
+
userid, project, name: "ITEMS",
|
|
92
|
+
alter: [{"field": "description", "width": 500}]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Add a new field
|
|
97
|
+
```bash
|
|
98
|
+
# Add 'created_at' field after 'id'
|
|
99
|
+
POST /alter {
|
|
100
|
+
userid, project, name: "RECORDS",
|
|
101
|
+
add: [{"name": "created_at", "width": 24, "after": "id"}]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Rename and widen
|
|
106
|
+
```bash
|
|
107
|
+
# Rename 'dept' to 'department' and widen to 50
|
|
108
|
+
POST /alter {
|
|
109
|
+
userid, project, name: "STAFF",
|
|
110
|
+
alter: [{"field": "dept", "rename": "department", "width": 50}]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Complex schema change
|
|
115
|
+
```bash
|
|
116
|
+
# Multiple operations in one call
|
|
117
|
+
POST /alter {
|
|
118
|
+
userid, project, name: "EMPLOYEES",
|
|
119
|
+
alter: [
|
|
120
|
+
{"field": "name", "width": 100},
|
|
121
|
+
{"field": "dept_id", "rename": "department_id"}
|
|
122
|
+
],
|
|
123
|
+
add: [
|
|
124
|
+
{"name": "email", "width": 80, "after": "name"},
|
|
125
|
+
{"name": "hire_date", "width": 10, "type": "text"}
|
|
126
|
+
],
|
|
127
|
+
drop: ["legacy_field", "unused_column"]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## How /alter Works Internally
|
|
134
|
+
|
|
135
|
+
1. **Reads the old format (.F)** to understand current field positions
|
|
136
|
+
2. **Creates a new format (.F)** with your requested changes
|
|
137
|
+
3. **Reads all records from the old data (.DA)**
|
|
138
|
+
4. **Reformats each record** to match the new schema:
|
|
139
|
+
- Widened fields: existing data left-aligned, space-padded
|
|
140
|
+
- Narrowed fields: truncated to new width
|
|
141
|
+
- New fields: filled with spaces
|
|
142
|
+
- Dropped fields: removed from record
|
|
143
|
+
5. **Writes reformatted records** to the new data file
|
|
144
|
+
6. **Replaces the old files** (if in-place) or creates new files (if `output` specified)
|
|
145
|
+
|
|
146
|
+
This is a **safe, non-destructive transformation** of existing data. No data is lost unless you explicitly narrow fields or drop them.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Common Gotchas
|
|
151
|
+
|
|
152
|
+
1. **Narrowing fields truncates data** — if you narrow a field from 100 to 50, any values longer than 50 characters will be truncated. Use `/format` first to check max field lengths.
|
|
153
|
+
|
|
154
|
+
2. **Field order matters for `after`** — when adding multiple fields, they're processed in order. If you add field B after field A, and also add field C after field A, field B will come first (because it was added first).
|
|
155
|
+
|
|
156
|
+
3. **Cannot rename and drop the same field** — if you rename a field, don't also include it in the `drop` array.
|
|
157
|
+
|
|
158
|
+
4. **Output file must not exist** — if you specify an `output` name and that file already exists, the operation will fail.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## See Also
|
|
163
|
+
|
|
164
|
+
- `/format` — inspect current schema before making changes
|
|
165
|
+
- `/import` — create NEW data files (DESTRUCTIVE if file exists)
|
|
166
|
+
- `/query` — query data after schema changes
|