datanotes 3.4.1 → 3.4.2

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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Column bindings of an extension: the manifest columns of a dependency role may be bound to
3
+ * columns of another name in the user's table (`--map role.column=column`). The manifest names
4
+ * columns in its view queries, CHECKs and link filters; this module rewrites those names to the
5
+ * bound ones, following link columns from role to role (`book.author.name`). Pure: no I/O.
6
+ *
7
+ * What it cannot rewrite safely it reports as an issue (fail closed), never guesses: a mapped
8
+ * column named in the outer clauses of a union view, or under a query key it does not know.
9
+ */
10
+ import type { ExtensionManifest, ManifestColumn } from "./extension-manifest.cjs";
11
+ /** role → (manifest column → bound column); only the columns whose names differ matter. */
12
+ export type ColumnMap = Readonly<Record<string, Readonly<Record<string, string>>>>;
13
+ /** A place where a mapped column is named and cannot be rewritten. */
14
+ export interface ColumnIssue {
15
+ role: string;
16
+ column: string;
17
+ /** Where it is named: `view vw_x (union order_by)`. */
18
+ where: string;
19
+ }
20
+ type Obj = Record<string, unknown>;
21
+ /** Rewrites the column names of bound roles; `issues` collects what it could not rewrite. */
22
+ export declare class ColumnBinder {
23
+ private readonly manifest;
24
+ readonly issues: ColumnIssue[];
25
+ private readonly map;
26
+ constructor(manifest: Pick<ExtensionManifest, "tables">, map: ColumnMap);
27
+ /** True when no role has a column bound to another name: every rewrite is the identity. */
28
+ get empty(): boolean;
29
+ /** The bound name of a role's manifest column. */
30
+ name(role: string | null, column: string): string;
31
+ /** The manifest columns of a role bound to another name. */
32
+ mapped(role: string): string[];
33
+ /** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
34
+ private linkRole;
35
+ /** A dotted column path read on `role`: each part on the role the previous link points to. */
36
+ path(role: string | null, dotted: string): string;
37
+ /** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
38
+ expr(role: string | null, e: string): string;
39
+ /** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
40
+ where(role: string | null, where: unknown): unknown;
41
+ /** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
42
+ column(role: string, c: ManifestColumn): ManifestColumn;
43
+ /** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
44
+ query(view: string, q: Obj): Obj;
45
+ private order;
46
+ /** `count`, `count:col`, `sum:meal.kcal`. */
47
+ private aggregate;
48
+ /** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
49
+ private join;
50
+ /** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
51
+ private union;
52
+ /** A clause the binder does not rewrite: an issue for each mapped column it names. */
53
+ private unknown;
54
+ /**
55
+ * The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
56
+ * one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
57
+ * role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
58
+ */
59
+ named(v: unknown, roles: string[], keys: (key: string, inKeys: boolean) => boolean): {
60
+ role: string;
61
+ column: string;
62
+ }[];
63
+ }
64
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Column bindings of an extension: the manifest columns of a dependency role may be bound to
3
+ * columns of another name in the user's table (`--map role.column=column`). The manifest names
4
+ * columns in its view queries, CHECKs and link filters; this module rewrites those names to the
5
+ * bound ones, following link columns from role to role (`book.author.name`). Pure: no I/O.
6
+ *
7
+ * What it cannot rewrite safely it reports as an issue (fail closed), never guesses: a mapped
8
+ * column named in the outer clauses of a union view, or under a query key it does not know.
9
+ */
10
+ import type { ExtensionManifest, ManifestColumn } from "./extension-manifest.js";
11
+ /** role → (manifest column → bound column); only the columns whose names differ matter. */
12
+ export type ColumnMap = Readonly<Record<string, Readonly<Record<string, string>>>>;
13
+ /** A place where a mapped column is named and cannot be rewritten. */
14
+ export interface ColumnIssue {
15
+ role: string;
16
+ column: string;
17
+ /** Where it is named: `view vw_x (union order_by)`. */
18
+ where: string;
19
+ }
20
+ type Obj = Record<string, unknown>;
21
+ /** Rewrites the column names of bound roles; `issues` collects what it could not rewrite. */
22
+ export declare class ColumnBinder {
23
+ private readonly manifest;
24
+ readonly issues: ColumnIssue[];
25
+ private readonly map;
26
+ constructor(manifest: Pick<ExtensionManifest, "tables">, map: ColumnMap);
27
+ /** True when no role has a column bound to another name: every rewrite is the identity. */
28
+ get empty(): boolean;
29
+ /** The bound name of a role's manifest column. */
30
+ name(role: string | null, column: string): string;
31
+ /** The manifest columns of a role bound to another name. */
32
+ mapped(role: string): string[];
33
+ /** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
34
+ private linkRole;
35
+ /** A dotted column path read on `role`: each part on the role the previous link points to. */
36
+ path(role: string | null, dotted: string): string;
37
+ /** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
38
+ expr(role: string | null, e: string): string;
39
+ /** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
40
+ where(role: string | null, where: unknown): unknown;
41
+ /** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
42
+ column(role: string, c: ManifestColumn): ManifestColumn;
43
+ /** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
44
+ query(view: string, q: Obj): Obj;
45
+ private order;
46
+ /** `count`, `count:col`, `sum:meal.kcal`. */
47
+ private aggregate;
48
+ /** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
49
+ private join;
50
+ /** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
51
+ private union;
52
+ /** A clause the binder does not rewrite: an issue for each mapped column it names. */
53
+ private unknown;
54
+ /**
55
+ * The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
56
+ * one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
57
+ * role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
58
+ */
59
+ named(v: unknown, roles: string[], keys: (key: string, inKeys: boolean) => boolean): {
60
+ role: string;
61
+ column: string;
62
+ }[];
63
+ }
64
+ export {};
@@ -48,7 +48,7 @@ export interface TableChoice {
48
48
  proposed: boolean;
49
49
  candidates: string[];
50
50
  }
51
- export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
51
+ export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "column_map_unsupported" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
52
52
  /** Something that keeps the plan from being written: what is wrong, how to fix it, where to read more. */
53
53
  export interface PlanProblem {
54
54
  code: ProblemCode;
@@ -48,7 +48,7 @@ export interface TableChoice {
48
48
  proposed: boolean;
49
49
  candidates: string[];
50
50
  }
51
- export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
51
+ export type ProblemCode = "engine_too_old" | "unknown_role" | "table_not_found" | "table_missing" | "table_exists" | "owned_create" | "ambiguous_dependency" | "missing_column" | "column_differs" | "owned_column_map" | "column_map_unsupported" | "migration_conflict" | "base_differs" | "view_exists" | "tool_name_taken" | "token_exists" | "too_many_operations" | "already_installed" | "downgrade" | "same_version";
52
52
  /** Something that keeps the plan from being written: what is wrong, how to fix it, where to read more. */
53
53
  export interface PlanProblem {
54
54
  code: ProblemCode;
@@ -38937,11 +38937,223 @@ var init_extension_host = __esm({
38937
38937
  }
38938
38938
  });
38939
38939
 
38940
+ // src/api/extension-columns.ts
38941
+ function roleOf(v) {
38942
+ if (typeof v !== "string") return null;
38943
+ const m = ROLE_REF.exec(v);
38944
+ return m ? m[1] : null;
38945
+ }
38946
+ var isObject5, LOGICAL2, DATE_EXPR3, QUERY_KEYS, JOIN_KEYS, UNION_PART_KEYS, ColumnBinder;
38947
+ var init_extension_columns = __esm({
38948
+ "src/api/extension-columns.ts"() {
38949
+ "use strict";
38950
+ init_extension_manifest();
38951
+ isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38952
+ LOGICAL2 = /* @__PURE__ */ new Set(["or", "and", "not"]);
38953
+ DATE_EXPR3 = /^date\((.+)\)([+-]\d+)?$/;
38954
+ QUERY_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where", "order_by", "limit", "offset", "expand", "join", "group_by", "aggregates", "include_body", "union"]);
38955
+ JOIN_KEYS = /* @__PURE__ */ new Set(["table", "on", "as", "type", "columns", "where"]);
38956
+ UNION_PART_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where"]);
38957
+ ColumnBinder = class {
38958
+ constructor(manifest, map) {
38959
+ this.manifest = manifest;
38960
+ this.issues = [];
38961
+ this.map = {};
38962
+ for (const [role, cols] of Object.entries(map)) {
38963
+ const differing = Object.entries(cols).filter(([from, to]) => from !== to);
38964
+ if (differing.length > 0) this.map[role] = Object.fromEntries(differing);
38965
+ }
38966
+ }
38967
+ /** True when no role has a column bound to another name: every rewrite is the identity. */
38968
+ get empty() {
38969
+ return Object.keys(this.map).length === 0;
38970
+ }
38971
+ /** The bound name of a role's manifest column. */
38972
+ name(role, column) {
38973
+ return role === null ? column : this.map[role]?.[column] ?? column;
38974
+ }
38975
+ /** The manifest columns of a role bound to another name. */
38976
+ mapped(role) {
38977
+ return Object.keys(this.map[role] ?? {});
38978
+ }
38979
+ /** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
38980
+ linkRole(role, column) {
38981
+ const c = typeof column === "string" ? this.manifest.tables[role]?.columns.find((x) => x.name === column) : column;
38982
+ if (!c || c.ref === void 0) return null;
38983
+ if (!(c.type === "link" || c.type === "list" && c.items === "link")) return null;
38984
+ return c.ref === "self" ? role : roleOf(c.ref);
38985
+ }
38986
+ /** A dotted column path read on `role`: each part on the role the previous link points to. */
38987
+ path(role, dotted) {
38988
+ if (role === null || this.empty) return dotted;
38989
+ const parts = dotted.split(".");
38990
+ const out2 = [];
38991
+ let cur = role;
38992
+ for (const part of parts) {
38993
+ out2.push(this.name(cur, part));
38994
+ cur = cur === null ? null : this.linkRole(cur, part);
38995
+ }
38996
+ return out2.join(".");
38997
+ }
38998
+ /** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
38999
+ expr(role, e) {
39000
+ const d = DATE_EXPR3.exec(e);
39001
+ return d ? `date(${this.path(role, d[1])})${d[2] ?? ""}` : this.path(role, e);
39002
+ }
39003
+ /** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
39004
+ where(role, where) {
39005
+ if (!isObject5(where) || role === null || this.empty) return where;
39006
+ const out2 = {};
39007
+ for (const [k, v] of Object.entries(where)) {
39008
+ if (LOGICAL2.has(k)) out2[k] = Array.isArray(v) ? v.map((w) => this.where(role, w)) : this.where(role, v);
39009
+ else out2[this.path(role, k)] = v;
39010
+ }
39011
+ return out2;
39012
+ }
39013
+ /** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
39014
+ column(role, c) {
39015
+ if (this.empty) return c;
39016
+ let out2 = c;
39017
+ const target = this.linkRole(role, c);
39018
+ if (c.ref_where !== void 0) out2 = { ...out2, ref_where: this.where(target, c.ref_where) };
39019
+ if (isObject5(c.ref_by)) {
39020
+ const by = c.ref_by;
39021
+ const col = this.name(role, String(by.column));
39022
+ const tgt = this.name(target, String(by.target_column ?? by.column));
39023
+ const { target_column: _t, ...rest } = by;
39024
+ out2 = { ...out2, ref_by: { ...rest, column: col, ...tgt !== col ? { target_column: tgt } : {} } };
39025
+ }
39026
+ if (typeof c.inverse === "string") out2 = { ...out2, inverse: this.name(target, c.inverse) };
39027
+ return out2;
39028
+ }
39029
+ /** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
39030
+ query(view, q) {
39031
+ if (this.empty) return q;
39032
+ if (Array.isArray(q.union)) return this.union(view, q);
39033
+ const role = roleOf(q.table);
39034
+ const out2 = {};
39035
+ const aliases = new Set(isObject5(q.aggregates) ? Object.keys(q.aggregates) : []);
39036
+ const col = (v) => aliases.has(v) ? v : this.path(role, v);
39037
+ for (const [k, v] of Object.entries(q)) {
39038
+ switch (k) {
39039
+ case "columns":
39040
+ case "expand":
39041
+ out2[k] = Array.isArray(v) ? v.map((x) => typeof x === "string" ? col(x) : x) : v;
39042
+ break;
39043
+ case "order_by":
39044
+ out2[k] = Array.isArray(v) ? v.map((x) => this.order(x, col)) : this.order(v, col);
39045
+ break;
39046
+ case "where":
39047
+ out2[k] = this.where(role, v);
39048
+ break;
39049
+ case "group_by":
39050
+ out2[k] = Array.isArray(v) ? v.map((x) => typeof x === "string" ? this.expr(role, x) : x) : typeof v === "string" ? this.expr(role, v) : v;
39051
+ break;
39052
+ case "aggregates":
39053
+ out2[k] = isObject5(v) ? Object.fromEntries(Object.entries(v).map(([a, spec2]) => [a, this.aggregate(role, spec2)])) : v;
39054
+ break;
39055
+ case "join":
39056
+ out2[k] = isObject5(v) ? this.join(view, role, v) : v;
39057
+ break;
39058
+ default:
39059
+ if (!QUERY_KEYS.has(k)) this.unknown(view, [role], k, v);
39060
+ out2[k] = v;
39061
+ }
39062
+ }
39063
+ return out2;
39064
+ }
39065
+ order(v, col) {
39066
+ if (typeof v !== "string") return v;
39067
+ return v.startsWith("-") ? `-${col(v.slice(1))}` : col(v);
39068
+ }
39069
+ /** `count`, `count:col`, `sum:meal.kcal`. */
39070
+ aggregate(role, spec2) {
39071
+ if (typeof spec2 !== "string") return spec2;
39072
+ const i = spec2.indexOf(":");
39073
+ return i < 0 ? spec2 : `${spec2.slice(0, i)}:${this.path(role, spec2.slice(i + 1))}`;
39074
+ }
39075
+ /** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
39076
+ join(view, left, j) {
39077
+ const right = roleOf(j.table);
39078
+ const out2 = {};
39079
+ for (const [k, v] of Object.entries(j)) {
39080
+ if (k === "on" && isObject5(v)) {
39081
+ out2[k] = Object.fromEntries(Object.entries(v).map(([a, b]) => [this.expr(left, a), typeof b === "string" ? this.expr(right, b) : b]));
39082
+ } else if (k === "columns" && Array.isArray(v)) out2[k] = v.map((x) => typeof x === "string" ? this.path(right, x) : x);
39083
+ else if (k === "where") out2[k] = this.where(right, v);
39084
+ else {
39085
+ if (!JOIN_KEYS.has(k)) this.unknown(view, [left, right], `join.${k}`, v);
39086
+ out2[k] = v;
39087
+ }
39088
+ }
39089
+ return out2;
39090
+ }
39091
+ /** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
39092
+ union(view, q) {
39093
+ const roles = [];
39094
+ const parts = q.union.map((p) => {
39095
+ if (!isObject5(p)) return p;
39096
+ const role = roleOf(p.table);
39097
+ roles.push(role);
39098
+ const out2 = {};
39099
+ for (const [k, v] of Object.entries(p)) {
39100
+ if (k === "columns" && Array.isArray(v)) out2[k] = v.map((x) => typeof x === "string" ? this.path(role, x) : x);
39101
+ else if (k === "where") out2[k] = this.where(role, v);
39102
+ else {
39103
+ if (!UNION_PART_KEYS.has(k)) this.unknown(view, [role], `union.${k}`, v);
39104
+ out2[k] = v;
39105
+ }
39106
+ }
39107
+ return out2;
39108
+ });
39109
+ for (const [k, v] of Object.entries(q)) if (k !== "union") this.unknown(view, roles, `union ${k}`, v);
39110
+ return { ...q, union: parts };
39111
+ }
39112
+ /** A clause the binder does not rewrite: an issue for each mapped column it names. */
39113
+ unknown(view, roles, key2, v) {
39114
+ for (const n of this.named(v, roles.filter((r) => r !== null), () => true)) this.issues.push({ ...n, where: `view ${view} (${key2})` });
39115
+ }
39116
+ /**
39117
+ * The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
39118
+ * one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
39119
+ * role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
39120
+ */
39121
+ named(v, roles, keys) {
39122
+ const words = /* @__PURE__ */ new Set();
39123
+ const tails = /* @__PURE__ */ new Set();
39124
+ const text5 = (x) => {
39125
+ for (const w of x.split(/[^A-Za-z0-9_.]+/)) {
39126
+ const parts = w.split(".");
39127
+ if (parts.length === 1) words.add(w);
39128
+ else for (const p of parts.slice(1)) tails.add(p);
39129
+ }
39130
+ };
39131
+ const walk = (x, inKeys) => {
39132
+ if (typeof x === "string") text5(x);
39133
+ else if (Array.isArray(x)) x.forEach((y) => walk(y, inKeys));
39134
+ else if (isObject5(x)) {
39135
+ for (const [k, y] of Object.entries(x)) {
39136
+ if (inKeys) text5(k);
39137
+ walk(y, keys(k, inKeys));
39138
+ }
39139
+ }
39140
+ };
39141
+ walk(v, keys("", false));
39142
+ const out2 = [];
39143
+ for (const role of Object.keys(this.map)) {
39144
+ for (const column of this.mapped(role)) if (tails.has(column) || roles.includes(role) && words.has(column)) out2.push({ role, column });
39145
+ }
39146
+ return out2;
39147
+ }
39148
+ };
39149
+ }
39150
+ });
39151
+
38940
39152
  // src/api/extension-plan.ts
38941
39153
  function stable(v) {
38942
39154
  const walk = (x) => {
38943
39155
  if (Array.isArray(x)) return x.map(walk);
38944
- if (isObject5(x)) {
39156
+ if (isObject6(x)) {
38945
39157
  const out2 = {};
38946
39158
  for (const k of Object.keys(x).sort()) out2[k] = walk(x[k]);
38947
39159
  return out2;
@@ -39115,6 +39327,10 @@ function planChanges(input) {
39115
39327
  for (const role of m.roles) tables[role] = choiceOf.get(role)?.table ?? `@${role}`;
39116
39328
  const bind = (v) => bindRoles(v, tables);
39117
39329
  const existing = (c) => c.decision === "adopt" || c.decision === "match";
39330
+ const columnMap = {};
39331
+ for (const c of input.chosen) if (!c.owned && c.decision === "match") columnMap[c.role] = { ...recorded[c.role]?.columns ?? {}, ...choices.columns[c.role] ?? {} };
39332
+ const binder = new ColumnBinder(m, columnMap);
39333
+ const bindColumn = (role, c) => bind(spec(binder.column(role, c)));
39118
39334
  const model = /* @__PURE__ */ new Map();
39119
39335
  for (const c of input.chosen) if (existing(c)) model.set(c.table, modelTable(input.described[c.table] ?? { qualified: c.table, columns: [] }));
39120
39336
  const views = new Set(input.listing.views.map((v) => v.qualified));
@@ -39142,6 +39358,19 @@ function planChanges(input) {
39142
39358
  );
39143
39359
  continue;
39144
39360
  }
39361
+ const touched = mappedNamed(raw);
39362
+ if (touched !== null) {
39363
+ migrationProblems.push(
39364
+ problem(
39365
+ "migration_conflict",
39366
+ `migration ${g.version}: ${raw.op} names column ${touched.column} of ${touched.role}, which reads your column ${binder.name(touched.role, touched.column)}`,
39367
+ `Rename your column to ${touched.column} with rename_column and run the command again without --map ${touched.role}.${touched.column}, or ask the extension's author.`,
39368
+ EXT_DOCS.update,
39369
+ { role: touched.role, column: touched.column }
39370
+ )
39371
+ );
39372
+ continue;
39373
+ }
39145
39374
  const op2 = bind(raw);
39146
39375
  const r = simulate(op2, g.version);
39147
39376
  if (typeof r === "string") {
@@ -39153,6 +39382,9 @@ function planChanges(input) {
39153
39382
  }
39154
39383
  }
39155
39384
  }
39385
+ function mappedNamed(raw) {
39386
+ return binder.named(raw, rolesNamed(raw), (k, inKeys) => inKeys || FILTER_KEYS.has(k))[0] ?? null;
39387
+ }
39156
39388
  function qualify(op2) {
39157
39389
  return `${String(op2.catalog ?? catalog)}.${String(op2.schema ?? viewSchema)}.${String(op2.name)}`;
39158
39390
  }
@@ -39285,7 +39517,7 @@ function planChanges(input) {
39285
39517
  schema,
39286
39518
  name: tname,
39287
39519
  description: def.description,
39288
- columns: bind(def.columns.map(spec)),
39520
+ columns: def.columns.map((c) => bindColumn(role, c)),
39289
39521
  ...def.filename !== void 0 ? { filename: def.filename } : {},
39290
39522
  ...def.aliases !== void 0 ? { aliases: def.aliases } : {}
39291
39523
  });
@@ -39294,8 +39526,8 @@ function planChanges(input) {
39294
39526
  }
39295
39527
  const actualCols = model.get(q).columns;
39296
39528
  for (const c of def.columns) {
39297
- const bound = bind(spec(c));
39298
- const actualName = choices.columns[role]?.[c.name] ?? recorded[role]?.columns?.[c.name] ?? c.name;
39529
+ const bound = bindColumn(role, c);
39530
+ const actualName = binder.name(role, c.name);
39299
39531
  const actual = actualCols.get(actualName);
39300
39532
  if (actual) {
39301
39533
  const diff = columnDifference(bound, actual, q, false);
@@ -39336,7 +39568,7 @@ function planChanges(input) {
39336
39568
  const q = t.table;
39337
39569
  const tags = { extension: name, extension_role: role };
39338
39570
  const instructions = def.instructions === void 0 ? void 0 : bind(def.instructions);
39339
- const constraint = (c) => ({ op: "add_constraint", table: q, name: c.name, check: bind(c.check), hint: c.hint });
39571
+ const constraint = (c) => ({ op: "add_constraint", table: q, name: c.name, check: bind(binder.where(role, c.check)), hint: c.hint });
39340
39572
  if (t.decision === "create") {
39341
39573
  const [cat, schema, tname] = q.split(".");
39342
39574
  operations.push({
@@ -39345,7 +39577,7 @@ function planChanges(input) {
39345
39577
  schema,
39346
39578
  name: tname,
39347
39579
  description: def.description,
39348
- columns: bind(def.columns.map(spec)),
39580
+ columns: def.columns.map((c) => bindColumn(role, c)),
39349
39581
  ...def.filename !== void 0 ? { filename: def.filename } : {},
39350
39582
  ...def.aliases !== void 0 ? { aliases: def.aliases } : {},
39351
39583
  ...instructions !== void 0 ? { instructions } : {},
@@ -39358,7 +39590,7 @@ function planChanges(input) {
39358
39590
  }
39359
39591
  const mt = model.get(q);
39360
39592
  for (const c of def.columns) {
39361
- const bound = bind(spec(c));
39593
+ const bound = bindColumn(role, c);
39362
39594
  const actual = mt.columns.get(c.name);
39363
39595
  if (!actual) {
39364
39596
  operations.push({ op: "add_column", table: q, column: bound });
@@ -39382,7 +39614,7 @@ function planChanges(input) {
39382
39614
  if (!actual) {
39383
39615
  operations.push(constraint(c));
39384
39616
  t.changes.push(`add CHECK ${c.name}`);
39385
- } else if (stable(actual.check) !== stable(bind(c.check)) || actual.hint !== c.hint) {
39617
+ } else if (stable(actual.check) !== stable(bind(binder.where(role, c.check))) || actual.hint !== c.hint) {
39386
39618
  operations.push({ op: "drop_constraint", table: q, name: c.name }, constraint(c));
39387
39619
  t.changes.push(`replace CHECK ${c.name}`);
39388
39620
  }
@@ -39410,13 +39642,14 @@ function planChanges(input) {
39410
39642
  for (const v of Object.keys(m.views)) {
39411
39643
  const def = m.views[v];
39412
39644
  const q = `${catalog}.${viewSchema}.${v}`;
39645
+ const query = binder.query(v, def.query);
39413
39646
  const create = {
39414
39647
  op: "create_view",
39415
39648
  catalog,
39416
39649
  schema: viewSchema,
39417
39650
  name: v,
39418
39651
  description: def.description,
39419
- query: bind(def.query),
39652
+ query: bind(query),
39420
39653
  ...def.aliases !== void 0 ? { aliases: def.aliases } : {},
39421
39654
  ...def.labels !== void 0 ? { labels: def.labels } : {}
39422
39655
  };
@@ -39425,7 +39658,7 @@ function planChanges(input) {
39425
39658
  viewPlan.changes.push(`create view ${v}`);
39426
39659
  } else if (action === "install") {
39427
39660
  reconcileProblems.push(problem("view_exists", `${q} exists already`, "Drop or rename that view (drop_view), or install with --schema <another schema>.", EXT_DOCS.install, { table: q }));
39428
- } else if (stable(oldManifest.views[v]) !== stable(def)) {
39661
+ } else if (stable(oldManifest.views[v]) !== stable(def) || stable(query) !== stable(def.query)) {
39429
39662
  operations.push({ ...create, replace: true });
39430
39663
  viewPlan.changes.push(`replace view ${v}`);
39431
39664
  }
@@ -39441,6 +39674,17 @@ function planChanges(input) {
39441
39674
  if (m.tables[role] === void 0) notes.push(`${role} (${rt.table}) is no longer used by the extension: it stays in the database`);
39442
39675
  }
39443
39676
  }
39677
+ for (const i of binder.issues) {
39678
+ reconcileProblems.push(
39679
+ problem(
39680
+ "column_map_unsupported",
39681
+ `${i.role}: ${i.where} names column ${i.column}, which reads your column ${binder.name(i.role, i.column)}: the installer cannot rewrite it there`,
39682
+ `Rename your column to ${i.column} with rename_column and leave --map ${i.role}.${i.column} out, or ask the extension's author.`,
39683
+ EXT_DOCS.tables,
39684
+ { role: i.role, column: i.column }
39685
+ )
39686
+ );
39687
+ }
39444
39688
  const toolProblems = [];
39445
39689
  const others = input.others.filter((o) => o.name !== name);
39446
39690
  const boundNames = (roles) => roles.map((r) => tables[r]);
@@ -39561,16 +39805,18 @@ function renderPlan(plan) {
39561
39805
  lines.push("", plan.agent_message, plan.summary);
39562
39806
  return lines.join("\n");
39563
39807
  }
39564
- var AGENT_MESSAGE, MAX_OPERATIONS, isObject5, obj3, list2, tableName, isLink;
39808
+ var AGENT_MESSAGE, FILTER_KEYS, MAX_OPERATIONS, isObject6, obj3, list2, tableName, isLink;
39565
39809
  var init_extension_plan = __esm({
39566
39810
  "src/api/extension-plan.ts"() {
39567
39811
  "use strict";
39812
+ init_extension_columns();
39568
39813
  init_extension_manifest();
39569
39814
  init_guidance();
39570
39815
  AGENT_MESSAGE = `For the agent installing this extension: show this plan to the user before anything is written. Where a table says "proposed", the installer chose it: ask the user to confirm it, to pick another table (--map <role>=<catalog.schema.table>, --map <role>.<column>=<column>), or to create it (--create <role>). Add a missing column only after the user agrees (--add <role>.<column>). Solve each problem the same way. Then run the same command again with the user's choices and --yes, only once the user approved the tables it creates or changes, the tools and the token's permissions.`;
39816
+ FILTER_KEYS = /* @__PURE__ */ new Set(["check", "where", "ref_where", "query", "on"]);
39571
39817
  MAX_OPERATIONS = 200;
39572
- isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
39573
- obj3 = (v) => isObject5(v) ? v : {};
39818
+ isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
39819
+ obj3 = (v) => isObject6(v) ? v : {};
39574
39820
  list2 = (v) => Array.isArray(v) ? v : [];
39575
39821
  tableName = (t, role) => t.name ?? role;
39576
39822
  isLink = (c) => c.type === "link" || c.type === "list" && c.items === "link";
@@ -40697,7 +40943,7 @@ __export(util_exports, {
40697
40943
  hexToUint8Array: () => hexToUint8Array,
40698
40944
  hide: () => hide,
40699
40945
  installLazyProp: () => installLazyProp,
40700
- isObject: () => isObject6,
40946
+ isObject: () => isObject7,
40701
40947
  isPlainObject: () => isPlainObject2,
40702
40948
  issue: () => issue,
40703
40949
  joinValues: () => joinValues,
@@ -40904,11 +41150,11 @@ function esc(str3) {
40904
41150
  function slugify(input) {
40905
41151
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
40906
41152
  }
40907
- function isObject6(data) {
41153
+ function isObject7(data) {
40908
41154
  return typeof data === "object" && data !== null && !Array.isArray(data);
40909
41155
  }
40910
41156
  function isPlainObject2(o) {
40911
- if (isObject6(o) === false)
41157
+ if (isObject7(o) === false)
40912
41158
  return false;
40913
41159
  const ctor = o.constructor;
40914
41160
  if (ctor === void 0)
@@ -40916,7 +41162,7 @@ function isPlainObject2(o) {
40916
41162
  if (typeof ctor !== "function")
40917
41163
  return true;
40918
41164
  const prot = ctor.prototype;
40919
- if (isObject6(prot) === false)
41165
+ if (isObject7(prot) === false)
40920
41166
  return false;
40921
41167
  if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
40922
41168
  return false;
@@ -43315,7 +43561,7 @@ var init_schemas = __esm({
43315
43561
  }
43316
43562
  return propValues;
43317
43563
  });
43318
- const isObject7 = isObject6;
43564
+ const isObject8 = isObject7;
43319
43565
  const catchall = def.catchall;
43320
43566
  let value;
43321
43567
  const memo2 = globalConfig.memoizer;
@@ -43323,7 +43569,7 @@ var init_schemas = __esm({
43323
43569
  inst._zod.parse = (payload, ctx) => {
43324
43570
  value ?? (value = _normalized.value);
43325
43571
  const input = payload.value;
43326
- if (!isObject7(input)) {
43572
+ if (!isObject8(input)) {
43327
43573
  payload.issues.push({
43328
43574
  expected: "object",
43329
43575
  code: "invalid_type",
@@ -43459,7 +43705,7 @@ var init_schemas = __esm({
43459
43705
  return doc.compile();
43460
43706
  };
43461
43707
  let fastpass;
43462
- const isObject7 = isObject6;
43708
+ const isObject8 = isObject7;
43463
43709
  const jit = !globalConfig.jitless;
43464
43710
  const allowsEval2 = allowsEval;
43465
43711
  const fastEnabled = jit && allowsEval2.value;
@@ -43468,7 +43714,7 @@ var init_schemas = __esm({
43468
43714
  inst._zod.parse = (payload, ctx) => {
43469
43715
  value ?? (value = _normalized.value);
43470
43716
  const input = payload.value;
43471
- if (!isObject7(input)) {
43717
+ if (!isObject8(input)) {
43472
43718
  payload.issues.push({
43473
43719
  expected: "object",
43474
43720
  code: "invalid_type",
@@ -43568,7 +43814,7 @@ var init_schemas = __esm({
43568
43814
  const disc = cached(() => discriminatorMap(def));
43569
43815
  inst._zod.parse = (payload, ctx) => {
43570
43816
  const input = payload.value;
43571
- if (!isObject6(input)) {
43817
+ if (!isObject7(input)) {
43572
43818
  payload.issues.push({
43573
43819
  code: "invalid_type",
43574
43820
  expected: "object",
@@ -77387,7 +77633,7 @@ function setUp(o, vault, home, explicit, self) {
77387
77633
  // src/node/service.ts
77388
77634
  init_local();
77389
77635
  init_hints();
77390
- var VERSION = true ? "3.4.1" : "dev";
77636
+ var VERSION = true ? "3.4.2" : "dev";
77391
77637
  function parsePort(raw, from = "--port") {
77392
77638
  const n = /^\s*\d+\s*$/.test(raw) ? Number(raw) : NaN;
77393
77639
  if (n === 0 || Number.isInteger(n) && n >= 1024 && n <= 65535) return n;
@@ -78452,7 +78698,7 @@ Docs: https://datanotes.dev`;
78452
78698
 
78453
78699
  // src/node/cli.ts
78454
78700
  init_message();
78455
- var VERSION2 = true ? "3.4.1" : "dev";
78701
+ var VERSION2 = true ? "3.4.2" : "dev";
78456
78702
  var consoleError = console.error;
78457
78703
  async function main5(argv = process.argv.slice(2)) {
78458
78704
  const [command, ...rest] = argv;
@@ -89,7 +89,7 @@ Five options carry your choices. They apply to `install` and, except the last tw
89
89
  | `--catalog` | Sets the catalog of created tables and views. | `--catalog work` |
90
90
  | `--schema` | Sets the schema of created owned tables and views. | `--schema reading` |
91
91
 
92
- Each option takes a comma-separated list. `--map role=catalog.schema.table` binds a table. `--map role.column=column` maps a column. A column of the extension’s own table keeps the extension’s name: you cannot map it.
92
+ Each option takes a comma-separated list. `--map role=catalog.schema.table` binds a table. `--map role.column=column` maps a column of a table the extension depends on. The views, CHECKs and link filters of the extension then read your column, through links too. A column of the extension’s own table keeps the extension’s name: you cannot map it.
93
93
 
94
94
  The plan ends with a message for the AI agent that runs the install:
95
95
 
@@ -101,6 +101,7 @@ An agent never guesses. It shows the plan, asks the user, and runs the command a
101
101
  * `ambiguous_dependency`: several tables could be the role. Pass `--map` or `--create`.
102
102
  * `missing_column`: the matched table lacks a column. Map another column, or add it with `--add`.
103
103
  * `column_differs`: a column exists with another type or rule. Map another column or table.
104
+ * `column_map_unsupported`: the extension names a mapped column where the installer cannot rewrite it, such as the outer clauses of a `union` view. Rename your column to the extension’s name and leave the mapping out.
104
105
  * `table_exists`: a table the extension would create exists already. Use it with `--map`, or pick another catalog.
105
106
  * `tool_name_taken`: a tool name is already an operation or a tool of another extension.
106
107
  * `token_exists`: a token named `ext-<name>` exists already.
package/dist/docs/api.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Datanotes HTTP API
2
2
 
3
- > OpenAPI 3.1.0 description of the Datanotes 3.4.1 HTTP API: https://datanotes.dev/openapi.json
3
+ > OpenAPI 3.1.0 description of the Datanotes 3.4.2 HTTP API: https://datanotes.dev/openapi.json
4
4
 
5
5
  Database of Markdown notes: tables of notes, rows, queries, history, a change feed and the vault's notes.
6
6