@malloydata/malloy 0.0.432 → 0.0.433

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.
@@ -22,38 +22,65 @@ const inSeconds = {
22
22
  day: 24 * 3600,
23
23
  week: 7 * 24 * 3600,
24
24
  };
25
+ /**
26
+ * MySQL reports a column type as
27
+ *
28
+ * base [ '(' params ')' ] [ 'unsigned' ] [ 'zerofill' ]
29
+ *
30
+ * and normalizes every alias away before reporting, so a schema read only
31
+ * ever produces canonical base names. A raw cast reaches this map without
32
+ * that normalization, and any alias it spells -- REAL, INTEGER, NUMERIC --
33
+ * falls through to `sql native`.
34
+ *
35
+ * No modifier changes the Malloy type: `unsigned` only widens the range, and
36
+ * the sole type whose range crosses a Malloy boundary is bigint, which is
37
+ * already `bigint`; `zerofill` is presentation. So the map is keyed on the
38
+ * base name alone. DECIMAL is not in the map because its Malloy type depends
39
+ * on its parameters.
40
+ */
25
41
  const mysqlToMalloyTypes = {
26
- // TODO: This assumes tinyint is always going to be a boolean.
27
- 'tinyint': { type: 'boolean' },
42
+ 'tinyint': { type: 'number', numberType: 'integer' },
28
43
  'smallint': { type: 'number', numberType: 'integer' },
29
44
  'mediumint': { type: 'number', numberType: 'integer' },
30
45
  'int': { type: 'number', numberType: 'integer' },
31
46
  'bigint': { type: 'number', numberType: 'bigint' },
32
- 'tinyint unsigned': { type: 'number', numberType: 'integer' },
33
- 'smallint unsigned': { type: 'number', numberType: 'integer' },
34
- 'mediumint unsigned': { type: 'number', numberType: 'integer' },
35
- 'int unsigned': { type: 'number', numberType: 'integer' },
36
- 'bigint unsigned': { type: 'number', numberType: 'bigint' },
47
+ 'float': { type: 'number', numberType: 'float' },
37
48
  'double': { type: 'number', numberType: 'float' },
38
- 'varchar': { type: 'string' },
39
- 'varbinary': { type: 'string' },
40
49
  'char': { type: 'string' },
50
+ 'varchar': { type: 'string' },
51
+ 'tinytext': { type: 'string' },
41
52
  'text': { type: 'string' },
53
+ 'mediumtext': { type: 'string' },
54
+ 'longtext': { type: 'string' },
42
55
  'date': { type: 'date' },
43
56
  'datetime': { type: 'timestamp' },
44
57
  'timestamp': { type: 'timestamp' },
45
58
  'time': { type: 'string' },
46
- 'decimal': { type: 'number', numberType: 'float' },
47
- // TODO: Check if we need special handling for boolean.
48
- 'tinyint(1)': { type: 'boolean' },
49
59
  };
60
+ /**
61
+ * Split a reported type into its base name and numeric parameters.
62
+ *
63
+ * Only numeric parameter lists are read. ENUM and SET carry quoted value
64
+ * lists whose quoting rules a regex should not chase, and nothing needs
65
+ * their values -- both map to `sql native`.
66
+ */
67
+ function parseMySQLType(sqlType) {
68
+ var _a, _b;
69
+ const text = sqlType.trim().toLowerCase();
70
+ const base = (_b = (_a = text.match(/^\w+/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : text;
71
+ const params = text.match(/^\w+\s*\((\d+(?:\s*,\s*\d+)*)\)/);
72
+ return {
73
+ base,
74
+ params: params ? params[1].split(',').map(p => parseInt(p, 10)) : [],
75
+ };
76
+ }
50
77
  function malloyTypeToJSONTableType(malloyType) {
51
78
  switch (malloyType.type) {
52
79
  case 'number':
53
- if (malloyType.numberType === 'integer') {
54
- return 'INT';
55
- }
56
- else if (malloyType.numberType === 'bigint') {
80
+ // BIGINT for both: JSON_TABLE turns an out-of-range value into NULL
81
+ // without warning, and `integer` is not bounded at 32 bits.
82
+ if (malloyType.numberType === 'integer' ||
83
+ malloyType.numberType === 'bigint') {
57
84
  return 'BIGINT';
58
85
  }
59
86
  else {
@@ -62,7 +89,8 @@ function malloyTypeToJSONTableType(malloyType) {
62
89
  case 'string':
63
90
  return 'CHAR(255)'; // JSON_TABLE needs a length
64
91
  case 'boolean':
65
- return 'INT'; // or TINYINT(1) if you prefer
92
+ // MySQL has no boolean; TINYINT(1) is only a spelling of TINYINT.
93
+ return 'INT';
66
94
  case 'record':
67
95
  case 'array':
68
96
  return 'JSON';
@@ -141,11 +169,24 @@ class MySQLDialect extends dialect_1.Dialect {
141
169
  }
142
170
  sqlTypeToMalloyType(sqlType) {
143
171
  var _a, _b;
144
- // Remove trailing params
145
- const baseSqlType = (_b = (_a = sqlType.match(/^(\w+)/)) === null || _a === void 0 ? void 0 : _a.at(0)) !== null && _b !== void 0 ? _b : sqlType;
146
- return (mysqlToMalloyTypes[baseSqlType.toLowerCase()] || {
172
+ const { base, params } = parseMySQLType(sqlType);
173
+ if (base === 'decimal') {
174
+ // DECIMAL is exact, so scale 0 is an integer rather than a float. Above
175
+ // 15 digits it no longer survives a JS double and must be carried as a
176
+ // bigint. MySQL reports precision and scale on every decimal; the
177
+ // defaults match its own when a bare DECIMAL is declared.
178
+ const [precision, scale] = [(_a = params[0]) !== null && _a !== void 0 ? _a : 10, (_b = params[1]) !== null && _b !== void 0 ? _b : 0];
179
+ if (scale > 0) {
180
+ return { type: 'number', numberType: 'float' };
181
+ }
182
+ return {
183
+ type: 'number',
184
+ numberType: precision <= 15 ? 'integer' : 'bigint',
185
+ };
186
+ }
187
+ return (mysqlToMalloyTypes[base] || {
147
188
  type: 'sql native',
148
- rawType: baseSqlType,
189
+ rawType: base,
149
190
  });
150
191
  }
151
192
  sqlGroupSetTable(groupSetCount) {
@@ -45,6 +45,7 @@ const static_space_1 = require("../field-space/static-space");
45
45
  const TDU = __importStar(require("../typedesc-utils"));
46
46
  const field_references_1 = require("../query-items/field-references");
47
47
  const expression_def_1 = require("../types/expression-def");
48
+ const field_space_1 = require("../types/field-space");
48
49
  const space_field_1 = require("../types/space-field");
49
50
  const expr_id_reference_1 = require("./expr-id-reference");
50
51
  class ExprAggregateFunction extends expression_def_1.ExpressionDef {
@@ -224,37 +225,30 @@ function joinPathEq(a1, a2) {
224
225
  }
225
226
  function getJoinUsage(fs, expr) {
226
227
  const result = [];
228
+ /**
229
+ * Walk a path from translated IR. Every name in it is known to resolve at
230
+ * private level, so a failure is an internal error. The names are
231
+ * unparented; readEntry records no references for them.
232
+ */
227
233
  const lookupWithPath = (fs, path) => {
228
- const head = path[0];
229
- const rest = path.slice(1);
230
- const def = fs.entry(head);
231
- if (def === undefined) {
232
- throw new Error(`Invalid field lookup ${head}`);
233
- }
234
- if (def instanceof static_space_1.StructSpaceField && rest.length > 0) {
235
- const restDef = lookupWithPath(def.fieldSpace, rest);
236
- return {
237
- ...restDef,
238
- joinPath: [{ ...def.joinPathElement, name: head }, ...restDef.joinPath],
239
- };
234
+ const names = path.map(n => new field_space_1.FieldName(n));
235
+ const last = names[names.length - 1];
236
+ const ns = (0, static_space_1.resolveNamespace)(fs, names.slice(0, -1), 'private', last.name);
237
+ if (ns.error) {
238
+ throw new Error(ns.error.message);
240
239
  }
241
- else if (def instanceof space_field_1.SpaceField) {
242
- if (rest.length !== 0) {
243
- throw new Error(`${head} cannot contain a ${rest.join('.')}`);
244
- }
245
- const fieldDef = def.fieldDef();
246
- if (fieldDef) {
247
- return {
248
- fs,
249
- def: fieldDef,
250
- joinPath: [],
251
- };
252
- }
253
- throw new Error('No field def');
240
+ const read = (0, static_space_1.readEntry)(ns.space, last, 'private');
241
+ if (read.error) {
242
+ throw new Error(read.error.message);
254
243
  }
255
- else {
244
+ if (!(read.found instanceof space_field_1.SpaceField)) {
256
245
  throw new Error('expected a field def or struct');
257
246
  }
247
+ const def = read.found.fieldDef();
248
+ if (def === undefined) {
249
+ throw new Error('No field def');
250
+ }
251
+ return { fs: ns.space, def, joinPath: ns.joinPath };
258
252
  };
259
253
  for (const frag of (0, utils_1.exprWalk)(expr)) {
260
254
  if (frag.node === 'field') {
@@ -23,6 +23,7 @@ class DefSpace extends passthrough_space_1.PassthroughSpace {
23
23
  error: {
24
24
  message: `Circular reference to '${this.circular.defineName}' in definition`,
25
25
  code: 'circular-reference-in-field-definition',
26
+ at: symbol[0],
26
27
  },
27
28
  found: undefined,
28
29
  };
@@ -12,7 +12,6 @@ const field_references_1 = require("../query-items/field-references");
12
12
  const space_field_1 = require("../types/space-field");
13
13
  const query_spaces_1 = require("./query-spaces");
14
14
  const reference_field_1 = require("./reference-field");
15
- const static_space_1 = require("./static-space");
16
15
  class IndexFieldSpace extends query_spaces_1.QueryOperationSpace {
17
16
  constructor() {
18
17
  super(...arguments);
@@ -81,46 +80,21 @@ class IndexFieldSpace extends query_spaces_1.QueryOperationSpace {
81
80
  }
82
81
  addRefineFromFields(_refineThis) { }
83
82
  addWild(wild) {
84
- var _a, _b;
85
- let current = this.exprSpace;
86
- const joinPath = [];
87
- if (wild.joinPath) {
88
- // walk path to determine namespace for *
89
- for (const pathPart of wild.joinPath.list) {
90
- const part = pathPart.refString;
91
- joinPath.push(part);
92
- const ent = current.entry(part);
93
- if (ent) {
94
- if (ent instanceof static_space_1.StructSpaceField) {
95
- current = ent.fieldSpace;
96
- }
97
- else {
98
- pathPart.logError('invalid-wildcard-source', `Field '${part}' does not contain rows and cannot be expanded with '*'`);
99
- return;
100
- }
101
- }
102
- else {
103
- pathPart.logError('wildcard-source-not-found', `No such field as '${part}'`);
104
- return;
105
- }
106
- }
83
+ var _a, _b, _c, _d;
84
+ const entries = this.wildcardExpansion(wild);
85
+ if (entries === undefined) {
86
+ return;
107
87
  }
88
+ const joinPath = (_b = (_a = wild.joinPath) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : [];
108
89
  const dialect = this.dialectObj();
109
90
  const expandEntries = [];
110
- for (const [name, entry] of current.entries()) {
111
- if (wild.except.has(name)) {
112
- continue;
113
- }
114
- if (entry.refType === 'parameter') {
115
- continue;
116
- }
91
+ for (const [name, entry] of entries) {
117
92
  const indexName = field_references_1.IndexFieldReference.indexOutputName([
118
93
  ...joinPath,
119
94
  name,
120
95
  ]);
121
96
  if (this.entry(indexName)) {
122
- const conflict = (_b = (_a = this.expandedWild.get(indexName)) === null || _a === void 0 ? void 0 : _a.path) === null || _b === void 0 ? void 0 : _b.join('.');
123
- wild.logError('name-conflict-in-wildcard-expansion', `Cannot expand '${name}' in '${wild.refString}' because a field with that name already exists${conflict ? ` (conflicts with ${conflict})` : ''}`);
97
+ this.logWildcardConflict(wild, name, (_d = (_c = this.expandedWild.get(indexName)) === null || _c === void 0 ? void 0 : _c.path) === null || _d === void 0 ? void 0 : _d.join('.'));
124
98
  }
125
99
  else {
126
100
  const eTypeDesc = entry.typeDesc();
@@ -41,6 +41,7 @@ class ParameterSpace {
41
41
  error: {
42
42
  message: `\`${name}\` is not defined`,
43
43
  code: 'parameter-not-found',
44
+ at: name,
44
45
  },
45
46
  found: undefined,
46
47
  };
@@ -52,6 +53,7 @@ class ParameterSpace {
52
53
  .slice(1)
53
54
  .join('.')}\``,
54
55
  code: 'invalid-parameter-reference',
56
+ at: name,
55
57
  },
56
58
  found: undefined,
57
59
  };
@@ -57,6 +57,16 @@ export declare abstract class QueryOperationSpace extends RefinedSpace implement
57
57
  inputSpace(): QueryInputSpace;
58
58
  outputSpace(): QueryOperationSpace;
59
59
  isQueryOutputSpace(): boolean;
60
+ protected logWildcardConflict(wild: WildcardFieldReference, name: string, conflict: string | undefined): void;
61
+ /**
62
+ * A `*` expands the fields a user of the finished source will see: it
63
+ * walks its path and reads the namespace at the end as a public reader,
64
+ * with the same walk and the same per-entry rule as a lookup by name.
65
+ * A disallowed path is an error, since the user wrote it; a disallowed
66
+ * field is dropped, since naming it would disclose it. Returns undefined,
67
+ * having logged, when the path fails or nothing is left to expand.
68
+ */
69
+ protected wildcardExpansion(wild: WildcardFieldReference): [string, SpaceEntry][] | undefined;
60
70
  protected addWild(wild: WildcardFieldReference): void;
61
71
  protected addValidatedCompositeFieldUserFromEntry(name: string, entry: SpaceEntry): void;
62
72
  addFieldUserFromFilter(filter: model.FilterCondition): void;
@@ -114,43 +114,44 @@ class QueryOperationSpace extends refined_space_1.RefinedSpace {
114
114
  isQueryOutputSpace() {
115
115
  return true;
116
116
  }
117
+ logWildcardConflict(wild, name, conflict) {
118
+ wild.logError('name-conflict-in-wildcard-expansion', `Cannot expand '${name}' in '${wild.refString}' because a field with that name already exists${conflict ? ` (conflicts with ${conflict})` : ''}`);
119
+ }
120
+ /**
121
+ * A `*` expands the fields a user of the finished source will see: it
122
+ * walks its path and reads the namespace at the end as a public reader,
123
+ * with the same walk and the same per-entry rule as a lookup by name.
124
+ * A disallowed path is an error, since the user wrote it; a disallowed
125
+ * field is dropped, since naming it would disclose it. Returns undefined,
126
+ * having logged, when the path fails or nothing is left to expand.
127
+ */
128
+ wildcardExpansion(wild) {
129
+ var _a, _b, _c;
130
+ const ns = (0, static_space_1.resolveNamespace)(this.exprSpace, (_b = (_a = wild.joinPath) === null || _a === void 0 ? void 0 : _a.list) !== null && _b !== void 0 ? _b : [], 'public', '*');
131
+ if (ns.error) {
132
+ const at = (_c = ns.error.at) !== null && _c !== void 0 ? _c : wild;
133
+ at.logError(ns.error.code, ns.error.message);
134
+ return undefined;
135
+ }
136
+ const entries = (0, static_space_1.accessibleEntries)(ns.space, ns.accessLevel).filter(([name, entry]) => !wild.except.has(name) && entry.refType !== 'parameter');
137
+ if (entries.length === 0) {
138
+ wild.logError('wildcard-matched-no-fields', `'${wild.refString}' did not match any fields`);
139
+ return undefined;
140
+ }
141
+ return entries;
142
+ }
117
143
  addWild(wild) {
118
- var _a;
119
- let current = this.exprSpace;
120
- const joinPath = [];
121
- if (wild.joinPath) {
122
- // walk path to determine namespace for *
123
- for (const pathPart of wild.joinPath.list) {
124
- const part = pathPart.refString;
125
- joinPath.push(part);
126
- const ent = current.entry(part);
127
- if (ent) {
128
- if (ent instanceof static_space_1.StructSpaceField) {
129
- current = ent.fieldSpace;
130
- }
131
- else {
132
- pathPart.logError('invalid-wildcard-source', `Field '${part}' does not contain rows and cannot be expanded with '*'`);
133
- return;
134
- }
135
- }
136
- else {
137
- pathPart.logError('wildcard-source-not-defined', `No such field as '${part}'`);
138
- return;
139
- }
140
- }
144
+ var _a, _b, _c;
145
+ const entries = this.wildcardExpansion(wild);
146
+ if (entries === undefined) {
147
+ return;
141
148
  }
149
+ const joinPath = (_b = (_a = wild.joinPath) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : [];
142
150
  const dialect = this.dialectObj();
143
151
  const expandEntries = [];
144
- for (const [name, entry] of current.entries()) {
145
- if (wild.except.has(name)) {
146
- continue;
147
- }
148
- if (entry.refType === 'parameter') {
149
- continue;
150
- }
152
+ for (const [name, entry] of entries) {
151
153
  if (this.entry(name)) {
152
- const conflict = (_a = this.expandedWild.get(name)) === null || _a === void 0 ? void 0 : _a.path.join('.');
153
- wild.logError('name-conflict-in-wildcard-expansion', `Cannot expand '${name}' in '${wild.refString}' because a field with that name already exists${conflict ? ` (conflicts with ${conflict})` : ''}`);
154
+ this.logWildcardConflict(wild, name, (_c = this.expandedWild.get(name)) === null || _c === void 0 ? void 0 : _c.path.join('.'));
154
155
  }
155
156
  else {
156
157
  const eType = entry.typeDesc();
@@ -1,7 +1,7 @@
1
1
  import type { Dialect } from '../../../dialect/dialect';
2
2
  import type { FieldDef, StructDef, SourceDef, JoinFieldDef, AccessModifierLabel } from '../../../model/malloy_types';
3
3
  import type { SpaceEntry } from '../types/space-entry';
4
- import type { LookupResult } from '../types/lookup-result';
4
+ import type { JoinPath, LookupError, LookupResult } from '../types/lookup-result';
5
5
  import type { FieldName, FieldSpace, QueryFieldSpace, SourceFieldSpace } from '../types/field-space';
6
6
  import { SpaceField } from '../types/space-field';
7
7
  import { StructSpaceFieldBase } from './struct-space-field-base';
@@ -50,3 +50,36 @@ export declare class StaticSourceSpace extends StaticSpace implements SourceFiel
50
50
  emptyStructDef(): SourceDef;
51
51
  accessProtectionLevel(): AccessModifierLabel;
52
52
  }
53
+ /**
54
+ * A namespace reached by walking a join path, and the access level a reader
55
+ * who started at `accessLevel` has once they arrive there.
56
+ */
57
+ export interface NamespaceRead {
58
+ space: FieldSpace;
59
+ accessLevel: AccessModifierLabel;
60
+ joinPath: JoinPath;
61
+ error: undefined;
62
+ }
63
+ /**
64
+ * Walk `path` from `from`, one `readEntry` per hop, narrowing the access
65
+ * level at each join. `lookup()` walks the path before a name with it and
66
+ * `*` walks the path before a star. `member` is what is about to be read from the namespace at the end
67
+ * of the path, a field name or `'*'`, named in the error when a hop is not a
68
+ * namespace.
69
+ */
70
+ export declare function resolveNamespace(from: FieldSpace, path: FieldName[], accessLevel: AccessModifierLabel, member: string): NamespaceRead | LookupError;
71
+ interface EntryRead {
72
+ found: SpaceEntry;
73
+ error: undefined;
74
+ }
75
+ /**
76
+ * Read one name from a namespace on behalf of a reader at `accessLevel`.
77
+ * Records the reference, and refuses the entry if the reader may not see it.
78
+ */
79
+ export declare function readEntry(space: FieldSpace, name: FieldName, accessLevel: AccessModifierLabel): EntryRead | LookupError;
80
+ /**
81
+ * Every entry of `space` a reader at `accessLevel` may see, by the same
82
+ * rule `readEntry` applies to one name.
83
+ */
84
+ export declare function accessibleEntries(space: FieldSpace, accessLevel: AccessModifierLabel): [string, SpaceEntry][];
85
+ export {};
@@ -5,6 +5,9 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.StaticSourceSpace = exports.StructSpaceField = exports.StaticSpace = void 0;
8
+ exports.resolveNamespace = resolveNamespace;
9
+ exports.readEntry = readEntry;
10
+ exports.accessibleEntries = accessibleEntries;
8
11
  const dialect_map_1 = require("../../../dialect/dialect_map");
9
12
  const malloy_types_1 = require("../../../model/malloy_types");
10
13
  const field_space_1 = require("../types/field-space");
@@ -107,81 +110,19 @@ class StaticSpace {
107
110
  }
108
111
  lookup(path, accessLevel) {
109
112
  accessLevel !== null && accessLevel !== void 0 ? accessLevel : (accessLevel = this.accessProtectionLevel());
110
- const head = path[0];
111
- const rest = path.slice(1);
112
- let found = this.entry(head.refString);
113
- if (!found) {
114
- return {
115
- error: {
116
- message: `'${head}' is not defined`,
117
- code: 'field-not-found',
118
- },
119
- found,
120
- };
113
+ const last = path[path.length - 1];
114
+ const ns = resolveNamespace(this, path.slice(0, -1), accessLevel, last.refString);
115
+ if (ns.error) {
116
+ return ns;
121
117
  }
122
- if (found instanceof space_field_1.SpaceField) {
123
- const definition = found.fieldDef();
124
- if (definition) {
125
- if (!(found instanceof struct_space_field_base_1.StructSpaceFieldBase) && (0, malloy_types_1.isJoined)(definition)) {
126
- // We have looked up a field which is a join, but not a StructSpaceField
127
- // because it is someting like "dimension: joinedArray is arrayComputation"
128
- // which wasn't known to be a join when the fieldspace was constructed.
129
- // TODO don't make one of these every time you do a lookup
130
- found = new StructSpaceField(definition, this.structDialect, this.structConnection);
131
- }
132
- // cswenson review todo I don't know how to count the reference properly now
133
- // i tried only writing it as a join reference if there was more in the path
134
- // but that failed because lookup([JOINNAME]) is called when translating JOINNAME.AGGREGATE(...)
135
- // with a 1-length-path but that IS a join reference and there is a test
136
- head.addReference({
137
- type: found instanceof struct_space_field_base_1.StructSpaceFieldBase
138
- ? 'joinReference'
139
- : 'fieldReference',
140
- definition: {
141
- type: definition.type,
142
- annotations: definition.annotations,
143
- location: definition.location,
144
- },
145
- location: head.location,
146
- text: head.refString,
147
- });
148
- }
149
- if (definition === null || definition === void 0 ? void 0 : definition.accessModifier) {
150
- if (!accessAllowed(accessLevel, definition.accessModifier)) {
151
- return {
152
- error: {
153
- message: `'${head}' is ${definition === null || definition === void 0 ? void 0 : definition.accessModifier}`,
154
- code: 'field-not-accessible',
155
- },
156
- found: undefined,
157
- };
158
- }
159
- }
160
- } // cswenson review todo { else this is SpaceEntry not a field which can only be a param and what is going on? }
161
- const joinPath = found instanceof struct_space_field_base_1.StructSpaceFieldBase
162
- ? [{ ...found.joinPathElement, name: head.refString }]
163
- : [];
164
- if (rest.length) {
165
- if (found instanceof struct_space_field_base_1.StructSpaceFieldBase) {
166
- const restResult = found.fieldSpace.lookup(rest, lessPermissiveAccessLevel(accessLevel, found.fieldSpace.accessProtectionLevel()));
167
- if (restResult.found) {
168
- return {
169
- ...restResult,
170
- joinPath: [...joinPath, ...restResult.joinPath],
171
- };
172
- }
173
- else {
174
- return restResult;
175
- }
176
- }
177
- return {
178
- error: {
179
- message: `'${head}' cannot contain a '${rest[0]}'`,
180
- code: 'invalid-property-access-in-field-reference',
181
- },
182
- found: undefined,
183
- };
118
+ const read = readEntry(ns.space, last, ns.accessLevel);
119
+ if (read.error) {
120
+ return read;
184
121
  }
122
+ const found = read.found;
123
+ const joinPath = found instanceof struct_space_field_base_1.StructSpaceFieldBase
124
+ ? [...ns.joinPath, { ...found.joinPathElement, name: last.refString }]
125
+ : ns.joinPath;
185
126
  return { found, error: undefined, joinPath, isOutputField: false };
186
127
  }
187
128
  isQueryFieldSpace() {
@@ -225,9 +166,111 @@ class StaticSourceSpace extends StaticSpace {
225
166
  }
226
167
  }
227
168
  exports.StaticSourceSpace = StaticSourceSpace;
169
+ /**
170
+ * Walk `path` from `from`, one `readEntry` per hop, narrowing the access
171
+ * level at each join. `lookup()` walks the path before a name with it and
172
+ * `*` walks the path before a star. `member` is what is about to be read from the namespace at the end
173
+ * of the path, a field name or `'*'`, named in the error when a hop is not a
174
+ * namespace.
175
+ */
176
+ function resolveNamespace(from, path, accessLevel, member) {
177
+ let space = from;
178
+ const joinPath = [];
179
+ for (let i = 0; i < path.length; i++) {
180
+ const hop = path[i];
181
+ const read = readEntry(space, hop, accessLevel);
182
+ if (read.error) {
183
+ return read;
184
+ }
185
+ if (!(read.found instanceof struct_space_field_base_1.StructSpaceFieldBase)) {
186
+ const next = i + 1 < path.length ? path[i + 1].refString : member;
187
+ const message = next === '*'
188
+ ? `'${hop}' does not contain fields and cannot be expanded with '*'`
189
+ : `'${hop}' cannot contain a '${next}'`;
190
+ return {
191
+ error: {
192
+ message,
193
+ code: 'invalid-property-access-in-field-reference',
194
+ at: hop,
195
+ },
196
+ found: undefined,
197
+ };
198
+ }
199
+ joinPath.push({ ...read.found.joinPathElement, name: hop.refString });
200
+ space = read.found.fieldSpace;
201
+ accessLevel = lessPermissiveAccessLevel(accessLevel, space.accessProtectionLevel());
202
+ }
203
+ return { space, accessLevel, joinPath, error: undefined };
204
+ }
205
+ /**
206
+ * Read one name from a namespace on behalf of a reader at `accessLevel`.
207
+ * Records the reference, and refuses the entry if the reader may not see it.
208
+ */
209
+ function readEntry(space, name, accessLevel) {
210
+ let found = space.entry(name.refString);
211
+ let restriction;
212
+ if (!found) {
213
+ return {
214
+ error: {
215
+ message: `'${name}' is not defined`,
216
+ code: 'field-not-found',
217
+ at: name,
218
+ },
219
+ found: undefined,
220
+ };
221
+ }
222
+ if (found instanceof space_field_1.SpaceField) {
223
+ const definition = found.fieldDef();
224
+ restriction = definition === null || definition === void 0 ? void 0 : definition.accessModifier;
225
+ if (definition) {
226
+ if (!(found instanceof struct_space_field_base_1.StructSpaceFieldBase) && (0, malloy_types_1.isJoined)(definition)) {
227
+ // A field which turned out to be a join after the space was built,
228
+ // e.g. "dimension: joinedArray is arrayComputation", so the entry
229
+ // is not a StructSpaceField; promote it so the path can continue.
230
+ found = new StructSpaceField(definition, space.dialectName(), space.connectionName());
231
+ }
232
+ // A one-element path to a join is still a join reference:
233
+ // JOIN.aggregate() looks the join up on its own.
234
+ name.addReference({
235
+ type: found instanceof struct_space_field_base_1.StructSpaceFieldBase
236
+ ? 'joinReference'
237
+ : 'fieldReference',
238
+ definition: {
239
+ type: definition.type,
240
+ annotations: definition.annotations,
241
+ location: definition.location,
242
+ },
243
+ location: name.location,
244
+ text: name.refString,
245
+ });
246
+ }
247
+ }
248
+ if (restriction && !accessAllowed(accessLevel, restriction)) {
249
+ return {
250
+ error: {
251
+ message: `'${name}' is ${restriction}`,
252
+ code: 'field-not-accessible',
253
+ at: name,
254
+ },
255
+ found: undefined,
256
+ };
257
+ }
258
+ return { found, error: undefined };
259
+ }
260
+ /**
261
+ * Every entry of `space` a reader at `accessLevel` may see, by the same
262
+ * rule `readEntry` applies to one name.
263
+ */
264
+ function accessibleEntries(space, accessLevel) {
265
+ return space.entries().filter(([, entry]) => {
266
+ var _a;
267
+ const restriction = entry instanceof space_field_1.SpaceField
268
+ ? (_a = entry.fieldDef()) === null || _a === void 0 ? void 0 : _a.accessModifier
269
+ : undefined;
270
+ return restriction === undefined || accessAllowed(accessLevel, restriction);
271
+ });
272
+ }
228
273
  function accessAllowed(accessLevel, accessModifier) {
229
- if (accessModifier === 'public')
230
- return true;
231
274
  if (accessLevel === 'internal')
232
275
  return accessModifier === 'internal';
233
276
  if (accessLevel === 'private')
@@ -1,6 +1,7 @@
1
1
  import type { JoinElementType, JoinType } from '../../../model';
2
2
  import type { MessageCode } from '../../parse-log';
3
3
  import type { SpaceEntry } from './space-entry';
4
+ import type { FieldName } from './field-space';
4
5
  export interface JoinPathElement {
5
6
  name: string;
6
7
  joinElementType: JoinElementType;
@@ -17,6 +18,7 @@ export interface LookupError {
17
18
  error: {
18
19
  message: string;
19
20
  code: MessageCode;
21
+ at?: FieldName | undefined;
20
22
  };
21
23
  found: undefined;
22
24
  }
@@ -154,12 +154,10 @@ type MessageParameterTypes = {
154
154
  'top-by-non-aggregate': string;
155
155
  'definition-name-conflict': string;
156
156
  'invalid-field-in-index-query': string;
157
- 'invalid-wildcard-source': string;
158
- 'wildcard-source-not-found': string;
159
157
  'name-conflict-in-wildcard-expansion': string;
160
158
  'invalid-parameter-reference': string;
161
159
  'parameter-not-found': string;
162
- 'wildcard-source-not-defined': string;
160
+ 'wildcard-matched-no-fields': string;
163
161
  'unexpected-index-segment': string;
164
162
  'accept-parameter': string;
165
163
  'except-parameter': string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.432";
1
+ export declare const MALLOY_VERSION = "0.0.433";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MALLOY_VERSION = void 0;
4
4
  // generated with 'generate-version-file' script; do not edit manually
5
- exports.MALLOY_VERSION = '0.0.432';
5
+ exports.MALLOY_VERSION = '0.0.433';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.432",
3
+ "version": "0.0.433",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -51,9 +51,9 @@
51
51
  "generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@malloydata/malloy-filter": "0.0.432",
55
- "@malloydata/malloy-interfaces": "0.0.432",
56
- "@malloydata/malloy-tag": "0.0.432",
54
+ "@malloydata/malloy-filter": "0.0.433",
55
+ "@malloydata/malloy-interfaces": "0.0.433",
56
+ "@malloydata/malloy-tag": "0.0.433",
57
57
  "@noble/hashes": "^1.8.0",
58
58
  "antlr4ts": "^0.5.0-alpha.4",
59
59
  "assert": "^2.0.0",