@malloydata/malloy 0.0.431 → 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.
- package/dist/api/foundation/config.js +23 -1
- package/dist/api/foundation/config_compile.d.ts +12 -1
- package/dist/api/foundation/config_compile.js +36 -25
- package/dist/api/foundation/config_lookup.d.ts +2 -2
- package/dist/api/foundation/config_lookup.js +32 -5
- package/dist/connection/registry.d.ts +43 -12
- package/dist/connection/registry.js +0 -18
- package/dist/connection/types.d.ts +7 -1
- package/dist/dialect/mysql/mysql.js +62 -21
- package/dist/lang/ast/expressions/expr-aggregate-function.js +20 -26
- package/dist/lang/ast/field-space/def-space.js +1 -0
- package/dist/lang/ast/field-space/index-field-space.js +7 -33
- package/dist/lang/ast/field-space/parameter-space.js +2 -0
- package/dist/lang/ast/field-space/query-spaces.d.ts +10 -0
- package/dist/lang/ast/field-space/query-spaces.js +33 -32
- package/dist/lang/ast/field-space/static-space.d.ts +34 -1
- package/dist/lang/ast/field-space/static-space.js +118 -75
- package/dist/lang/ast/types/lookup-result.d.ts +2 -0
- package/dist/lang/parse-log.d.ts +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.MalloyConfig = exports.Manifest = void 0;
|
|
8
8
|
exports.isBuildManifestEntry = isBuildManifestEntry;
|
|
9
|
+
const registry_1 = require("../../connection/registry");
|
|
9
10
|
const config_compile_1 = require("./config_compile");
|
|
10
11
|
const config_resolve_1 = require("./config_resolve");
|
|
11
12
|
const config_lookup_1 = require("./config_lookup");
|
|
@@ -165,7 +166,7 @@ class MalloyConfig {
|
|
|
165
166
|
// it happens async at `lookupConnection` time, so overlays that touch
|
|
166
167
|
// IO (secret stores, session reads) have a natural async seam.
|
|
167
168
|
const prepared = (0, config_resolve_1.prepareConfig)(compiled.compiled, mergedOverlays, log);
|
|
168
|
-
this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, mergedOverlays, log);
|
|
169
|
+
this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, rawConnectionEntries(pojo), mergedOverlays, log);
|
|
169
170
|
this._connections = this._managedLookup;
|
|
170
171
|
this._overlays = mergedOverlays;
|
|
171
172
|
this.virtualMap = toVirtualMap(prepared.virtualMap, log);
|
|
@@ -476,6 +477,27 @@ function toVirtualMap(raw, log) {
|
|
|
476
477
|
function isRecord(value) {
|
|
477
478
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
478
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* The connection entries exactly as authored, kept alongside the compiled tree
|
|
482
|
+
* and handed to each factory as `rawConfigData`. Compilation replaces an
|
|
483
|
+
* overlay reference with a `ConfigReference` node and resolution replaces that
|
|
484
|
+
* with the value, so this is the only place a connector can still see *which*
|
|
485
|
+
* overlay a value came from — which is what BigQuery's `getDigest` needs to
|
|
486
|
+
* tell two impersonated identities apart.
|
|
487
|
+
*
|
|
488
|
+
* No validation here beyond the shape: entries the compiler rejected are
|
|
489
|
+
* absent from `compiledConnections` and so are never looked up.
|
|
490
|
+
*/
|
|
491
|
+
function rawConnectionEntries(pojo) {
|
|
492
|
+
const entries = {};
|
|
493
|
+
if (!isRecord(pojo) || !isRecord(pojo['connections']))
|
|
494
|
+
return entries;
|
|
495
|
+
for (const [name, entry] of Object.entries(pojo['connections'])) {
|
|
496
|
+
if ((0, registry_1.isConnectionConfigEntry)(entry))
|
|
497
|
+
entries[name] = entry;
|
|
498
|
+
}
|
|
499
|
+
return entries;
|
|
500
|
+
}
|
|
479
501
|
function isBuildManifestEntry(value) {
|
|
480
502
|
return isRecord(value) && typeof value['tableName'] === 'string';
|
|
481
503
|
}
|
|
@@ -3,7 +3,7 @@ import type { LogMessage } from '../../lang/parse-log';
|
|
|
3
3
|
* A compiled config node. The compiler produces a tree of these; the resolver
|
|
4
4
|
* walks the tree against a ConfigOverlays dict and produces a plain POJO.
|
|
5
5
|
*/
|
|
6
|
-
export type ConfigNode = ConfigDict | ConfigLiteral | ConfigReference;
|
|
6
|
+
export type ConfigNode = ConfigDict | ConfigLiteral | ConfigReference | ConfigInvalid;
|
|
7
7
|
export interface ConfigDict {
|
|
8
8
|
kind: 'dict';
|
|
9
9
|
entries: Record<string, ConfigNode>;
|
|
@@ -23,6 +23,17 @@ export interface ConfigReference {
|
|
|
23
23
|
/** Path into the overlay. */
|
|
24
24
|
path: string[];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A property the user wrote that can never produce a value — it failed its
|
|
28
|
+
* type check. Compilation records it rather than dropping it so that a
|
|
29
|
+
* `mustHaveValue` property can tell "the user wrote something unusable" apart
|
|
30
|
+
* from "the user wrote nothing." It resolves to `undefined` like any other
|
|
31
|
+
* failure; the difference is only that the key is still visible in the
|
|
32
|
+
* compiled tree.
|
|
33
|
+
*/
|
|
34
|
+
export interface ConfigInvalid {
|
|
35
|
+
kind: 'invalid';
|
|
36
|
+
}
|
|
26
37
|
export type SectionCompiler = (value: unknown, log: LogMessage[]) => ConfigNode | undefined;
|
|
27
38
|
/**
|
|
28
39
|
* Compile a POJO into a typed dictionary tree, collecting validation warnings.
|
|
@@ -110,34 +110,49 @@ function compileConnectionEntry(prefix, typeName, rawEntry, log) {
|
|
|
110
110
|
return entry;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
* Compile a single connection property value.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
113
|
+
* Compile a single connection property value.
|
|
114
|
+
*
|
|
115
|
+
* At non-`json` property slots, a single-key object whose value is a string or
|
|
116
|
+
* string[] is recognized as an overlay reference. `json`-typed slots always
|
|
117
|
+
* pass through as literal data — this is the security invariant that prevents
|
|
118
|
+
* reference injection into structured config.
|
|
119
|
+
*
|
|
120
|
+
* A property's `source` overrides that reading in both directions. `'literal'`
|
|
121
|
+
* refuses a reference; `'overlay'` refuses a literal, which is also what lets
|
|
122
|
+
* a reference be recognized in a slot that would otherwise hold a dictionary.
|
|
118
123
|
*/
|
|
119
124
|
function compileConnectionProperty(path, propDef, value, log) {
|
|
120
|
-
if (value === undefined || value === null)
|
|
121
|
-
return undefined;
|
|
122
|
-
if (propDef.type === 'json') {
|
|
123
|
-
return { kind: 'value', value };
|
|
125
|
+
if (value === undefined || value === null) {
|
|
126
|
+
return propDef.mustHaveValue ? { kind: 'invalid' } : undefined;
|
|
124
127
|
}
|
|
125
128
|
const ref = asReferenceShape(value);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
switch (propDef.source) {
|
|
130
|
+
case 'overlay':
|
|
131
|
+
if (ref === undefined) {
|
|
132
|
+
log.push(makeWarning(path, 'must name an overlay, as in {env: "NAME"}; a value written here ' +
|
|
133
|
+
'directly is not allowed'));
|
|
134
|
+
return { kind: 'invalid' };
|
|
135
|
+
}
|
|
136
|
+
return ref;
|
|
137
|
+
case 'literal':
|
|
138
|
+
if (ref !== undefined) {
|
|
139
|
+
log.push(makeWarning(path, 'must be written here directly and cannot name an overlay'));
|
|
140
|
+
return { kind: 'invalid' };
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
if (propDef.type === 'json') {
|
|
145
|
+
return { kind: 'value', value };
|
|
146
|
+
}
|
|
147
|
+
if (ref !== undefined) {
|
|
148
|
+
return ref;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
132
151
|
}
|
|
133
152
|
const typeError = checkValueType(value, propDef.type);
|
|
134
153
|
if (typeError) {
|
|
135
|
-
if (propDef.requireLiteralString) {
|
|
136
|
-
log.push(makeWarning(path, `must be a literal string, got ${describeConfigValue(value)}`));
|
|
137
|
-
return { kind: 'value', value };
|
|
138
|
-
}
|
|
139
154
|
log.push(makeWarning(path, `${typeError} (expected ${propDef.type})`));
|
|
140
|
-
return undefined;
|
|
155
|
+
return propDef.mustHaveValue ? { kind: 'invalid' } : undefined;
|
|
141
156
|
}
|
|
142
157
|
return { kind: 'value', value };
|
|
143
158
|
}
|
|
@@ -221,11 +236,6 @@ function makeWarning(path, message) {
|
|
|
221
236
|
code: 'config-validation',
|
|
222
237
|
};
|
|
223
238
|
}
|
|
224
|
-
function describeConfigValue(value) {
|
|
225
|
-
if (Array.isArray(value))
|
|
226
|
-
return 'array';
|
|
227
|
-
return typeof value;
|
|
228
|
-
}
|
|
229
239
|
function checkValueType(value, expectedType) {
|
|
230
240
|
switch (expectedType) {
|
|
231
241
|
case 'number':
|
|
@@ -245,6 +255,7 @@ function checkValueType(value, expectedType) {
|
|
|
245
255
|
return `should be a string, got ${typeof value}`;
|
|
246
256
|
break;
|
|
247
257
|
case 'json':
|
|
258
|
+
case 'opaque':
|
|
248
259
|
break;
|
|
249
260
|
}
|
|
250
261
|
return undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LogMessage } from '../../lang/parse-log';
|
|
2
|
-
import type { ManagedConnectionLookup } from '../../connection/registry';
|
|
2
|
+
import type { ConnectionConfigEntry, ManagedConnectionLookup } from '../../connection/registry';
|
|
3
3
|
import type { ConfigDict } from './config_compile';
|
|
4
4
|
import type { ConfigOverlays } from './config_overlays';
|
|
5
5
|
/**
|
|
@@ -28,4 +28,4 @@ import type { ConfigOverlays } from './config_overlays';
|
|
|
28
28
|
* intentional consequence of deferred resolution: we don't pay for
|
|
29
29
|
* warnings on connections nobody asks about.
|
|
30
30
|
*/
|
|
31
|
-
export declare function buildManagedLookup(compiledConnections: Record<string, ConfigDict>, overlays: ConfigOverlays, log: LogMessage[]): ManagedConnectionLookup;
|
|
31
|
+
export declare function buildManagedLookup(compiledConnections: Record<string, ConfigDict>, rawConnections: Record<string, ConnectionConfigEntry>, overlays: ConfigOverlays, log: LogMessage[]): ManagedConnectionLookup;
|
|
@@ -32,7 +32,7 @@ const registry_1 = require("../../connection/registry");
|
|
|
32
32
|
* intentional consequence of deferred resolution: we don't pay for
|
|
33
33
|
* warnings on connections nobody asks about.
|
|
34
34
|
*/
|
|
35
|
-
function buildManagedLookup(compiledConnections, overlays, log) {
|
|
35
|
+
function buildManagedLookup(compiledConnections, rawConnections, overlays, log) {
|
|
36
36
|
const entries = Object.entries(compiledConnections);
|
|
37
37
|
const firstConnectionName = entries.length > 0 ? entries[0][0] : undefined;
|
|
38
38
|
const cache = new Map();
|
|
@@ -51,7 +51,7 @@ function buildManagedLookup(compiledConnections, overlays, log) {
|
|
|
51
51
|
if (!compiledEntry) {
|
|
52
52
|
throw new Error(`No connection named "${connectionName}" found in config`);
|
|
53
53
|
}
|
|
54
|
-
const resolved = await resolveCompiledEntry(compiledEntry, overlays, log);
|
|
54
|
+
const resolved = await resolveCompiledEntry(connectionName, compiledEntry, overlays, log);
|
|
55
55
|
// compileConnections guarantees `is` is present and a string-valued
|
|
56
56
|
// literal node — resolveCompiledEntry preserves it. Defensive check
|
|
57
57
|
// in case a compiler bug sneaks through.
|
|
@@ -71,8 +71,7 @@ function buildManagedLookup(compiledConnections, overlays, log) {
|
|
|
71
71
|
connConfig[key] = value;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
const connection = await typeDef.factory(connConfig);
|
|
74
|
+
const connection = await typeDef.factory(connConfig, rawConnections[connectionName]);
|
|
76
75
|
cache.set(connectionName, connection);
|
|
77
76
|
return connection;
|
|
78
77
|
},
|
|
@@ -97,7 +96,7 @@ function buildManagedLookup(compiledConnections, overlays, log) {
|
|
|
97
96
|
* followed by property-default application. Returns the plain POJO that
|
|
98
97
|
* gets handed to the factory.
|
|
99
98
|
*/
|
|
100
|
-
async function resolveCompiledEntry(entry, overlays, log) {
|
|
99
|
+
async function resolveCompiledEntry(connectionName, entry, overlays, log) {
|
|
101
100
|
const resolved = await resolveNode(entry, overlays, log);
|
|
102
101
|
// resolveNode returns `unknown`. The compileConnections pipeline
|
|
103
102
|
// guarantees every connection entry is an object dict with `is` set to
|
|
@@ -106,9 +105,35 @@ async function resolveCompiledEntry(entry, overlays, log) {
|
|
|
106
105
|
if (!(0, registry_1.isConnectionConfigEntry)(resolved)) {
|
|
107
106
|
throw new Error('Connection entry did not resolve to a valid {is: string, ...} dict');
|
|
108
107
|
}
|
|
108
|
+
// Before defaults, so that a declared default can't quietly satisfy a
|
|
109
|
+
// promise the user's own value failed to keep.
|
|
110
|
+
requireAuthoredValues(connectionName, entry, resolved);
|
|
109
111
|
await applyPropertyDefaults(resolved, overlays);
|
|
110
112
|
return resolved;
|
|
111
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Enforce `mustHaveValue`: a property the user wrote must survive resolution.
|
|
116
|
+
*
|
|
117
|
+
* The compiled entry is the record of what was authored — a reference that
|
|
118
|
+
* resolved to nothing, or a value that failed its type check, is gone from
|
|
119
|
+
* `resolved` but still present here. Absence from both is fine; that's the
|
|
120
|
+
* user declining to set the property, which is what `optional` already means.
|
|
121
|
+
*/
|
|
122
|
+
function requireAuthoredValues(connectionName, entry, resolved) {
|
|
123
|
+
var _a;
|
|
124
|
+
for (const prop of (_a = (0, registry_1.getConnectionProperties)(resolved.is)) !== null && _a !== void 0 ? _a : []) {
|
|
125
|
+
if (!prop.mustHaveValue)
|
|
126
|
+
continue;
|
|
127
|
+
if (entry.entries[prop.name] === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
if (resolved[prop.name] !== undefined)
|
|
130
|
+
continue;
|
|
131
|
+
throw new Error(`Connection "${connectionName}" sets "${prop.name}", but no value ` +
|
|
132
|
+
'arrived — an overlay reference that did not resolve, or a value that ' +
|
|
133
|
+
'was rejected. Omitting the property is allowed; this one cannot fall ' +
|
|
134
|
+
'back silently.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
112
137
|
/**
|
|
113
138
|
* Walk a node, awaiting any overlay calls. References that fail to resolve
|
|
114
139
|
* return `undefined`; the parent dict walker drops the corresponding
|
|
@@ -119,6 +144,8 @@ async function resolveNode(node, overlays, log) {
|
|
|
119
144
|
switch (node.kind) {
|
|
120
145
|
case 'value':
|
|
121
146
|
return node.value;
|
|
147
|
+
case 'invalid':
|
|
148
|
+
return undefined;
|
|
122
149
|
case 'reference':
|
|
123
150
|
return resolveReference(node, overlays, log);
|
|
124
151
|
case 'dict': {
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import type { Connection, ConnectionConfig, LookupConnection } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* A factory function that creates a Connection from a config object.
|
|
4
|
+
*
|
|
5
|
+
* `rawConfigData` is the connection's entry exactly as the user wrote it,
|
|
6
|
+
* before overlay references were resolved — so an overlay-supplied property
|
|
7
|
+
* still reads as `{tenantAuth: "acme"}` rather than as the value it produced.
|
|
8
|
+
* A connector needs it when a resolved value has no stable identity of its own
|
|
9
|
+
* but the connection's digest depends on which one it is; BigQuery's
|
|
10
|
+
* `authClient` is the case that exists today. It is undefined for hosts that
|
|
11
|
+
* build connections from an already-resolved config.
|
|
12
|
+
*
|
|
13
|
+
* Being unresolved, it is the *non-secret* twin of `config`: it holds
|
|
14
|
+
* `{env: "PGPASSWORD"}` where `config` holds the password.
|
|
4
15
|
*/
|
|
5
|
-
export type ConnectionTypeFactory = (config: ConnectionConfig) => Promise<Connection>;
|
|
16
|
+
export type ConnectionTypeFactory = (config: ConnectionConfig, rawConfigData?: ConnectionConfigEntry) => Promise<Connection>;
|
|
6
17
|
/**
|
|
7
18
|
* The type of a connection property value.
|
|
8
19
|
*/
|
|
9
|
-
export type ConnectionPropertyType = 'string' | 'number' | 'boolean' | 'password' | 'secret' | 'file' | 'json' | 'text'
|
|
20
|
+
export type ConnectionPropertyType = 'string' | 'number' | 'boolean' | 'password' | 'secret' | 'file' | 'json' | 'text'
|
|
21
|
+
/**
|
|
22
|
+
* A value with no declared shape that Malloy never inspects. Only reachable
|
|
23
|
+
* through an overlay — pair it with `source: 'overlay'`, since a config file
|
|
24
|
+
* has no way to write a live object down.
|
|
25
|
+
*/
|
|
26
|
+
| 'opaque';
|
|
10
27
|
/**
|
|
11
28
|
* Describes a single configuration property for a connection type.
|
|
12
29
|
*
|
|
@@ -35,12 +52,31 @@ export interface ConnectionPropertyDefinition {
|
|
|
35
52
|
/** For type 'file': extension filters for picker dialogs. */
|
|
36
53
|
fileFilters?: Record<string, string[]>;
|
|
37
54
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
55
|
+
* Where this property's value is allowed to come from. Unset means either —
|
|
56
|
+
* a literal in the config file, or an overlay reference.
|
|
57
|
+
*
|
|
58
|
+
* `'literal'` refuses overlay references. For a property whose value is only
|
|
59
|
+
* meaningful if it was written down deliberately.
|
|
60
|
+
*
|
|
61
|
+
* `'overlay'` refuses literals: the config file can only *name* an overlay
|
|
62
|
+
* the host registered, never supply the value itself. This is also what
|
|
63
|
+
* makes a reference unambiguous in a slot that would otherwise hold a
|
|
64
|
+
* dictionary — if a literal isn't legal there, a single-key object can only
|
|
65
|
+
* be a reference.
|
|
42
66
|
*/
|
|
43
|
-
|
|
67
|
+
source?: 'literal' | 'overlay';
|
|
68
|
+
/**
|
|
69
|
+
* Writing this property is a promise that it will produce a value. Omitting
|
|
70
|
+
* it is fine; setting it and having nothing survive resolution is an error
|
|
71
|
+
* rather than a silent drop.
|
|
72
|
+
*
|
|
73
|
+
* For properties whose absence triggers a fallback that isn't safe — DuckDB's
|
|
74
|
+
* `securityPolicy`, where absent means the `none` policy, or a credential
|
|
75
|
+
* whose absence means the ambient one. Without this, a misspelled overlay
|
|
76
|
+
* source or an unset environment variable removes the property and the
|
|
77
|
+
* connection is built as if the user had never asked for it.
|
|
78
|
+
*/
|
|
79
|
+
mustHaveValue?: true;
|
|
44
80
|
}
|
|
45
81
|
/**
|
|
46
82
|
* A connection type definition: factory plus property metadata.
|
|
@@ -106,11 +142,6 @@ export declare function getRegisteredConnectionTypes(): string[];
|
|
|
106
142
|
* lookup to hand fully-resolved configs to the right factory.
|
|
107
143
|
*/
|
|
108
144
|
export declare function getConnectionTypeDef(typeName: string): ConnectionTypeDef | undefined;
|
|
109
|
-
/**
|
|
110
|
-
* Enforce registry-level literal-string requirements after overlay resolution
|
|
111
|
-
* and before a connection factory sees the config.
|
|
112
|
-
*/
|
|
113
|
-
export declare function validateConnectionConfigProperties(connectionName: string, typeName: string, config: ConnectionConfig): void;
|
|
114
145
|
/**
|
|
115
146
|
* Parse a JSON config string into a ConnectionsConfig.
|
|
116
147
|
* Entries without a valid `is` field are silently dropped.
|
|
@@ -10,7 +10,6 @@ exports.getConnectionProperties = getConnectionProperties;
|
|
|
10
10
|
exports.getConnectionTypeDisplayName = getConnectionTypeDisplayName;
|
|
11
11
|
exports.getRegisteredConnectionTypes = getRegisteredConnectionTypes;
|
|
12
12
|
exports.getConnectionTypeDef = getConnectionTypeDef;
|
|
13
|
-
exports.validateConnectionConfigProperties = validateConnectionConfigProperties;
|
|
14
13
|
exports.readConnectionsConfig = readConnectionsConfig;
|
|
15
14
|
exports.writeConnectionsConfig = writeConnectionsConfig;
|
|
16
15
|
exports.createConnectionsFromConfig = createConnectionsFromConfig;
|
|
@@ -67,22 +66,6 @@ function getRegisteredConnectionTypes() {
|
|
|
67
66
|
function getConnectionTypeDef(typeName) {
|
|
68
67
|
return registry.get(typeName);
|
|
69
68
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Enforce registry-level literal-string requirements after overlay resolution
|
|
72
|
-
* and before a connection factory sees the config.
|
|
73
|
-
*/
|
|
74
|
-
function validateConnectionConfigProperties(connectionName, typeName, config) {
|
|
75
|
-
var _a, _b;
|
|
76
|
-
const props = (_b = (_a = registry.get(typeName)) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : [];
|
|
77
|
-
for (const prop of props) {
|
|
78
|
-
if (!prop.requireLiteralString)
|
|
79
|
-
continue;
|
|
80
|
-
const value = config[prop.name];
|
|
81
|
-
if (value !== undefined && typeof value !== 'string') {
|
|
82
|
-
throw new Error(`Connection "${connectionName}" property "${prop.name}" must be a literal string`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
69
|
/**
|
|
87
70
|
* Parse a JSON config string into a ConnectionsConfig.
|
|
88
71
|
* Entries without a valid `is` field are silently dropped.
|
|
@@ -144,7 +127,6 @@ function createConnectionsFromConfig(config, onConnectionCreated) {
|
|
|
144
127
|
connConfig[key] = value;
|
|
145
128
|
}
|
|
146
129
|
}
|
|
147
|
-
validateConnectionConfigProperties(connectionName, entry.is, connConfig);
|
|
148
130
|
const connection = await typeDef.factory(connConfig);
|
|
149
131
|
if (onConnectionCreated) {
|
|
150
132
|
onConnectionCreated(connectionName, connection);
|
|
@@ -49,9 +49,15 @@ export interface InfoConnection {
|
|
|
49
49
|
export type ConnectionParameterValue = string | number | boolean | null | Array<ConnectionParameterValue> | {
|
|
50
50
|
[key: string]: ConnectionParameterValue;
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* A value with no declared shape, supplied by a host overlay rather than
|
|
54
|
+
* written in a config file — an auth client, a session handle, anything live.
|
|
55
|
+
* Malloy never inspects one; it carries it from the overlay to the factory.
|
|
56
|
+
*/
|
|
57
|
+
export type OpaqueConnectionValue = object;
|
|
52
58
|
export interface ConnectionConfig {
|
|
53
59
|
name: string;
|
|
54
|
-
[key: string]: ConnectionParameterValue | undefined;
|
|
60
|
+
[key: string]: ConnectionParameterValue | OpaqueConnectionValue | undefined;
|
|
55
61
|
}
|
|
56
62
|
export interface ConnectionMetadata {
|
|
57
63
|
url?: string;
|
|
@@ -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
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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:
|
|
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
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
throw new Error(
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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') {
|
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
}
|
package/dist/lang/parse-log.d.ts
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
-
"@malloydata/malloy-interfaces": "0.0.
|
|
56
|
-
"@malloydata/malloy-tag": "0.0.
|
|
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",
|