@malloydata/malloy 0.0.374 → 0.0.376

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.
@@ -4,227 +4,113 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.resolveConfig = resolveConfig;
7
+ exports.prepareConfig = prepareConfig;
8
8
  const registry_1 = require("../../connection/registry");
9
9
  /**
10
- * Walk a compiled tree against the overlay dict and produce a plain
11
- * resolved POJO.
10
+ * Synchronous top-level walk of a compiled config tree. Extracts the
11
+ * non-connection sections (which only contain literals — see the section
12
+ * compilers) and hands back the compiled connection subtrees untouched.
12
13
  *
13
- * Two distinct "defaults" mechanisms, deliberately separated:
14
+ * Reference resolution for connection properties is *not* done here. It is
15
+ * deferred until `lookupConnection` is called, at which point the async
16
+ * walker in `config_lookup.ts` can `await` overlays that do IO (secret
17
+ * stores, session fetches, etc.). This keeps `MalloyConfig` construction
18
+ * synchronous and zero-IO.
14
19
  *
15
- * 1. **Property defaults** (`applyPropertyDefaults`) fill in missing
16
- * properties on *every* connection entry, user-listed or fabricated.
17
- * This is a uniform per-property rule; there is no asymmetry between
18
- * explicit and auto-generated entries.
19
- *
20
- * 2. **`includeDefaultConnections`** (`fabricateMissingConnections`) —
21
- * fabricate a bare `{is: typeName}` entry for each registered backend
22
- * not already represented. Property defaults then fill in their
23
- * properties via (1).
24
- *
25
- * Order matters: fabrication runs before property defaults so that
26
- * fabricated entries pick up defaults in the same pass as user-listed
27
- * ones.
28
- *
29
- * Three unresolved-reference cases, each with different handling:
30
- * 1. Unknown overlay source → warning, drop property
31
- * 2. Known overlay → undefined → silent drop
32
- * 3. Property default → unresolved (either of the above inside a default)
33
- * → silent drop (a default is a hint, not a requirement)
20
+ * Fabrication of bare `{is: typeName}` compiled entries for registered
21
+ * backends not otherwise represented happens here when the config opts in
22
+ * via `includeDefaultConnections`. Property defaults are filled in at
23
+ * lookup time alongside reference resolution.
34
24
  */
35
- function resolveConfig(compiled, overlays, log) {
36
- const resolved = { connections: {} };
25
+ function prepareConfig(compiled, _log) {
26
+ let compiledConnections = {};
27
+ let manifestPath;
28
+ let virtualMap;
37
29
  let includeDefaultConnections = false;
38
30
  for (const [key, node] of Object.entries(compiled.entries)) {
39
31
  switch (key) {
40
32
  case 'connections': {
41
33
  if (node.kind !== 'dict')
42
34
  break;
43
- resolved.connections = resolveConnections(node, overlays, log);
35
+ compiledConnections = extractCompiledConnections(node);
44
36
  break;
45
37
  }
46
38
  case 'manifestPath': {
47
- const v = resolveNode(node, overlays, log);
48
- if (typeof v === 'string')
49
- resolved.manifestPath = v;
39
+ if (node.kind === 'value' && typeof node.value === 'string') {
40
+ manifestPath = node.value;
41
+ }
50
42
  break;
51
43
  }
52
44
  case 'virtualMap': {
53
- // virtualMap is literal datathe class body converts it to the
54
- // runtime Map-of-Maps representation.
55
- resolved.virtualMap = resolveNode(node, overlays, log);
45
+ // virtualMap is a literal dict slot compileVirtualMap never
46
+ // produces a reference node. MalloyConfig converts the raw POJO
47
+ // into its runtime Map-of-Maps representation.
48
+ if (node.kind === 'value')
49
+ virtualMap = node.value;
56
50
  break;
57
51
  }
58
52
  case 'includeDefaultConnections': {
59
- const v = resolveNode(node, overlays, log);
60
- if (typeof v === 'boolean')
61
- includeDefaultConnections = v;
53
+ if (node.kind === 'value' && typeof node.value === 'boolean') {
54
+ includeDefaultConnections = node.value;
55
+ }
62
56
  break;
63
57
  }
64
58
  }
65
59
  }
66
60
  if (includeDefaultConnections) {
67
- fabricateMissingConnections(resolved.connections);
61
+ fabricateMissingConnections(compiledConnections);
68
62
  }
69
- // Property defaults apply to every entry — user-listed and fabricated
70
- // alike. This is the fix for the earlier bug where defaults only fired
71
- // during fabrication, leaving explicit entries silently underconfigured.
72
- applyPropertyDefaults(resolved.connections, overlays);
73
- return resolved;
63
+ return { compiledConnections, manifestPath, virtualMap };
74
64
  }
75
- // =============================================================================
76
- // Generic walk
77
- // =============================================================================
78
65
  /**
79
- * Walk a single node and produce its resolved value. References that fail
80
- * to resolve return `undefined`; the parent dict walker then drops the
81
- * corresponding property.
66
+ * Pull each well-formed compiled connection entry out of the `connections`
67
+ * subtree. Entries are already validated by `compileConnections` anything
68
+ * shaped wrong was dropped or reported during compile. We defensively skip
69
+ * non-dict children here anyway.
82
70
  */
83
- function resolveNode(node, overlays, log) {
84
- switch (node.kind) {
85
- case 'value':
86
- return node.value;
87
- case 'reference':
88
- return resolveReference(node, overlays, log);
89
- case 'dict': {
90
- const out = {};
91
- for (const [k, child] of Object.entries(node.entries)) {
92
- const r = resolveNode(child, overlays, log);
93
- if (r !== undefined)
94
- out[k] = r;
95
- }
96
- return out;
97
- }
98
- }
99
- }
100
- function resolveReference(ref, overlays, log) {
101
- const overlay = overlays[ref.source];
102
- if (!overlay) {
103
- // Case 1: unknown overlay source — warn and drop.
104
- log.push({
105
- message: `unknown overlay source "${ref.source}" for reference path ${JSON.stringify(ref.path)}`,
106
- severity: 'warn',
107
- code: 'config-overlay',
108
- });
109
- return undefined;
110
- }
111
- // Case 2: overlay returns undefined — silent drop (no log push).
112
- return overlay(ref.path);
113
- }
114
- // =============================================================================
115
- // Connections
116
- // =============================================================================
117
- function resolveConnections(node, overlays, log) {
118
- const result = {};
119
- for (const [name, connNode] of Object.entries(node.entries)) {
120
- if (connNode.kind !== 'dict')
121
- continue;
122
- const resolved = resolveNode(connNode, overlays, log);
123
- // compileConnectionEntry guarantees `is` is a string value node, and
124
- // resolveNode preserves it. Any connection without `is` is a bug in the
125
- // compiler; skip it defensively.
126
- if (typeof resolved['is'] !== 'string')
127
- continue;
128
- result[name] = resolved;
71
+ function extractCompiledConnections(node) {
72
+ const out = {};
73
+ for (const [name, child] of Object.entries(node.entries)) {
74
+ if (child.kind === 'dict')
75
+ out[name] = child;
129
76
  }
130
- return result;
77
+ return out;
131
78
  }
132
- // =============================================================================
133
- // Fabrication and property defaults
134
- // =============================================================================
135
79
  /**
136
- * Fabricate a bare `{is: typeName}` entry for each registered connection
137
- * type not already represented in `connections`. Only runs when the
138
- * `includeDefaultConnections` flag is set on the config. Property values
139
- * are *not* filled in here that is the job of `applyPropertyDefaults`,
140
- * which runs unconditionally on every entry in a later pass.
80
+ * Add a bare `{is: typeName}` compiled entry for each registered connection
81
+ * type not already represented in `compiledConnections`. Only runs when the
82
+ * POJO sets `includeDefaultConnections: true`. Property values (including
83
+ * reference-shaped defaults like DuckDB's `{config: 'rootDirectory'}`) are
84
+ * *not* filled in here that is the job of the async lookup resolver.
141
85
  *
142
- * A user-named connection that happens to share the type name but points
143
- * at a different backend is left alone.
86
+ * Skip rules:
87
+ * - Type already in use: some existing entry has `is: typeName`.
88
+ * - Name already taken: some existing entry is *named* `typeName`, even
89
+ * if its `is` points elsewhere. This protects a user who writes
90
+ * `{duckdb: {is: 'postgres', ...}}` — naming an entry after a type but
91
+ * pointing at a different backend — from being clobbered.
144
92
  *
145
- * Mutates `connections` in place. Called only by `resolveConfig` on its
146
- * own freshly-built object.
93
+ * Mutates `compiledConnections` in place.
147
94
  */
148
- function fabricateMissingConnections(connections) {
95
+ function fabricateMissingConnections(compiledConnections) {
149
96
  const presentTypes = new Set();
150
- for (const entry of Object.values(connections)) {
151
- if (typeof entry.is === 'string')
152
- presentTypes.add(entry.is);
97
+ for (const entry of Object.values(compiledConnections)) {
98
+ const isNode = entry.entries['is'];
99
+ if ((isNode === null || isNode === void 0 ? void 0 : isNode.kind) === 'value' && typeof isNode.value === 'string') {
100
+ presentTypes.add(isNode.value);
101
+ }
153
102
  }
154
103
  for (const typeName of (0, registry_1.getRegisteredConnectionTypes)()) {
155
- // `presentTypes` catches {mydb: {is: 'duckdb'}}; the name check catches
156
- // {duckdb: {is: 'jsondb'}}. Both cases leave the registered `duckdb`
157
- // type alone — the first because it's already represented, the second
158
- // because we can't use the obvious name without clobbering.
159
104
  if (presentTypes.has(typeName))
160
105
  continue;
161
- if (connections[typeName])
106
+ if (compiledConnections[typeName])
162
107
  continue;
163
- connections[typeName] = { is: typeName };
108
+ compiledConnections[typeName] = {
109
+ kind: 'dict',
110
+ entries: {
111
+ is: { kind: 'value', value: typeName },
112
+ },
113
+ };
164
114
  }
165
115
  }
166
- /**
167
- * For every connection entry, fill in any property that the user didn't
168
- * specify and whose `ConnectionPropertyDefinition` declares a `default`.
169
- * Runs uniformly on both user-listed and fabricated entries — the earlier
170
- * behavior of only firing during fabrication was a bug that left explicit
171
- * entries silently underconfigured (e.g. a user-listed `duckdb` never
172
- * picked up `workingDirectory: {config: 'rootDirectory'}`).
173
- *
174
- * Defaults that are reference-shaped resolve through the overlays via
175
- * `resolveDefault`. Unresolved defaults are silently dropped (case 3).
176
- * User-specified values are never overwritten.
177
- *
178
- * Interaction with inline references: if the user specified a property
179
- * as a reference-shaped value that failed to resolve (e.g. `{env: 'UNSET'}`),
180
- * `resolveConnections` already dropped it before we see the entry. From
181
- * our perspective the property is simply absent, so the default applies —
182
- * effectively turning inline references into "try this first, else fall
183
- * back to the default." This is almost always what users want.
184
- *
185
- * Mutates `connections` in place. Called only by `resolveConfig` on its
186
- * own freshly-built object.
187
- */
188
- function applyPropertyDefaults(connections, overlays) {
189
- var _a;
190
- for (const entry of Object.values(connections)) {
191
- const typeName = entry.is;
192
- if (typeof typeName !== 'string')
193
- continue;
194
- const props = (_a = (0, registry_1.getConnectionProperties)(typeName)) !== null && _a !== void 0 ? _a : [];
195
- for (const prop of props) {
196
- if (prop.default === undefined)
197
- continue;
198
- if (entry[prop.name] !== undefined)
199
- continue;
200
- const v = resolveDefault(prop.default, overlays);
201
- if (v !== undefined)
202
- entry[prop.name] = v;
203
- }
204
- }
205
- }
206
- /**
207
- * Resolve a property `default` field. Literals pass through; single-key
208
- * reference-shaped objects are resolved through the overlays. Case 3:
209
- * an unresolved default is a hint, not a requirement — always silent drop.
210
- */
211
- function resolveDefault(def, overlays) {
212
- if (typeof def !== 'object')
213
- return def;
214
- const keys = Object.keys(def);
215
- if (keys.length !== 1)
216
- return undefined;
217
- const source = keys[0];
218
- const raw = def[source];
219
- const path = typeof raw === 'string' ? [raw] : raw;
220
- // The type says `raw` is `string | string[]`, but `default` comes from
221
- // registered backend definitions which are runtime-dynamic — a
222
- // malformed registration would blow up inside the overlay otherwise.
223
- if (!Array.isArray(path))
224
- return undefined;
225
- const overlay = overlays[source];
226
- if (!overlay)
227
- return undefined;
228
- return overlay(path);
229
- }
230
116
  //# sourceMappingURL=config_resolve.js.map
@@ -87,6 +87,12 @@ export declare function getConnectionTypeDisplayName(typeName: string): string |
87
87
  * Get the names of all registered connection types.
88
88
  */
89
89
  export declare function getRegisteredConnectionTypes(): string[];
90
+ /**
91
+ * Get the full definition (factory + properties + displayName) for a
92
+ * registered connection type. Used by the foundation layer's connection
93
+ * lookup to hand fully-resolved configs to the right factory.
94
+ */
95
+ export declare function getConnectionTypeDef(typeName: string): ConnectionTypeDef | undefined;
90
96
  /**
91
97
  * Parse a JSON config string into a ConnectionsConfig.
92
98
  * Entries without a valid `is` field are silently dropped.
@@ -9,6 +9,7 @@ exports.registerConnectionType = registerConnectionType;
9
9
  exports.getConnectionProperties = getConnectionProperties;
10
10
  exports.getConnectionTypeDisplayName = getConnectionTypeDisplayName;
11
11
  exports.getRegisteredConnectionTypes = getRegisteredConnectionTypes;
12
+ exports.getConnectionTypeDef = getConnectionTypeDef;
12
13
  exports.readConnectionsConfig = readConnectionsConfig;
13
14
  exports.writeConnectionsConfig = writeConnectionsConfig;
14
15
  exports.createConnectionsFromConfig = createConnectionsFromConfig;
@@ -57,6 +58,14 @@ function getConnectionTypeDisplayName(typeName) {
57
58
  function getRegisteredConnectionTypes() {
58
59
  return [...registry.keys()];
59
60
  }
61
+ /**
62
+ * Get the full definition (factory + properties + displayName) for a
63
+ * registered connection type. Used by the foundation layer's connection
64
+ * lookup to hand fully-resolved configs to the right factory.
65
+ */
66
+ function getConnectionTypeDef(typeName) {
67
+ return registry.get(typeName);
68
+ }
60
69
  /**
61
70
  * Parse a JSON config string into a ConnectionsConfig.
62
71
  * Entries without a valid `is` field are silently dropped.
@@ -420,22 +420,18 @@ class DuckDBTypeParser extends tiny_parser_1.TinyParser {
420
420
  return token.text.toUpperCase();
421
421
  }
422
422
  typeDef() {
423
- const wantID = this.next('id');
423
+ var _a, _b;
424
+ const wantID = this.expect('id');
424
425
  const id = this.sqlID(wantID);
425
426
  let baseType;
426
427
  if (id === 'VARCHAR') {
427
- if (this.peek().type === 'size') {
428
- this.next();
429
- }
428
+ this.match('size');
430
429
  }
431
- if ((id === 'DECIMAL' || id === 'NUMERIC') &&
432
- this.peek().type === 'precision') {
433
- this.next();
430
+ if ((id === 'DECIMAL' || id === 'NUMERIC') && this.match('precision')) {
434
431
  baseType = { type: 'number', numberType: 'float' };
435
432
  }
436
433
  else if (id === 'TIMESTAMP') {
437
- if (this.peek().text.toUpperCase() === 'WITH') {
438
- this.nextText('WITH', 'TIME', 'ZONE');
434
+ if (this.matchText('WITH', 'TIME', 'ZONE')) {
439
435
  baseType = { type: 'timestamptz' };
440
436
  }
441
437
  else {
@@ -446,24 +442,21 @@ class DuckDBTypeParser extends tiny_parser_1.TinyParser {
446
442
  baseType = duckDBToMalloyTypes[id];
447
443
  }
448
444
  else if (id === 'STRUCT') {
449
- this.next('(');
445
+ this.expect('(');
450
446
  baseType = { type: 'record', fields: [] };
451
447
  for (;;) {
452
- const fieldName = this.next();
453
- if (fieldName.type === 'qsingle' ||
454
- fieldName.type === 'qdouble' ||
455
- fieldName.type === 'id') {
456
- const fieldType = this.typeDef();
457
- baseType.fields.push((0, malloy_types_1.mkFieldDef)(fieldType, this.unquoteName(fieldName)));
458
- }
459
- else {
460
- if (fieldName.type !== ')') {
448
+ const fieldName = (_b = (_a = this.match('qsingle')) !== null && _a !== void 0 ? _a : this.match('qdouble')) !== null && _b !== void 0 ? _b : this.match('id');
449
+ if (!fieldName) {
450
+ if (!this.match(')')) {
461
451
  throw this.parseError('Expected identifier or ) to end STRUCT');
462
452
  }
463
453
  break;
464
454
  }
465
- if (this.peek().type === ',') {
466
- this.next();
455
+ const fieldType = this.typeDef();
456
+ baseType.fields.push((0, malloy_types_1.mkFieldDef)(fieldType, this.unquoteName(fieldName)));
457
+ if (!this.match(',')) {
458
+ this.expect(')');
459
+ break;
467
460
  }
468
461
  }
469
462
  }
@@ -472,10 +465,8 @@ class DuckDBTypeParser extends tiny_parser_1.TinyParser {
472
465
  // unknown field type, strip all type decorations, there was a regex for this
473
466
  // in the pre-parser code, but no tests, so this is also untested
474
467
  let idEnd = wantID.cursor + wantID.text.length;
475
- if (this.peek().type === 'precision') {
476
- this.next();
477
- }
478
- if (this.peek().type === 'eof') {
468
+ this.match('precision');
469
+ if (this.eof()) {
479
470
  idEnd = this.input.length;
480
471
  }
481
472
  baseType = {
@@ -487,8 +478,7 @@ class DuckDBTypeParser extends tiny_parser_1.TinyParser {
487
478
  throw this.parseError('Could not understand type');
488
479
  }
489
480
  }
490
- while (this.peek().type === 'arrayOf') {
491
- this.next();
481
+ while (this.match('arrayOf')) {
492
482
  if (baseType.type === 'record') {
493
483
  baseType = {
494
484
  type: 'array',
@@ -4,9 +4,41 @@ export interface TinyToken {
4
4
  text: string;
5
5
  }
6
6
  /**
7
- * Simple framework for writing schema parsers. The parsers using this felt
8
- * better than the more ad-hoc code they replaced, and are smaller than
9
- * using a parser generator.
7
+ * Tiny combined lexer/parser for short recursive-descent parsers.
8
+ *
9
+ * TinyParser is intentionally small and biased toward readability over
10
+ * framework features. It is primarily used for schema and type parsers where
11
+ * a hand-written parser is clearer than ad-hoc regex matching, but a parser
12
+ * generator would be overkill.
13
+ *
14
+ * Design goals:
15
+ * - Keep parser implementations short and readable.
16
+ * - Support custom tokenization rules per parser.
17
+ * - Make parser intent obvious at call sites.
18
+ * - Minimize hidden consumption and parser-state surprises.
19
+ *
20
+ * Core parser API:
21
+ * - peek(): inspect the next token without consuming it.
22
+ * - read(): consume and return the next token, regardless of type.
23
+ * - eof(): true if the next token is end-of-input.
24
+ * - expect(...types): consume a required sequence of token types.
25
+ * - expectText(...texts): consume a required sequence of token texts.
26
+ * - match(...types): consume an optional sequence of token types.
27
+ * - matchText(...texts): consume an optional sequence of token texts.
28
+ *
29
+ * Semantics:
30
+ * - expect*() is for required grammar and throws on failure.
31
+ * - match*() is for optional grammar and is atomic. If the full sequence does
32
+ * not match, nothing is consumed.
33
+ * - peek() remains available, but most optional syntax should prefer match*().
34
+ *
35
+ * Token rules are tested in order. The first matching rule wins.
36
+ *
37
+ * Special token rule names:
38
+ * - space: matched text is skipped and never returned.
39
+ * - char: matched text becomes both token.type and token.text.
40
+ * - q*: any token name starting with q is treated as quoted text and has its
41
+ * first and last characters stripped from token.text.
10
42
  *
11
43
  * NOTE: All parse errors are exceptions.
12
44
  */
@@ -14,33 +46,36 @@ export declare class TinyParseError extends Error {
14
46
  }
15
47
  export declare class TinyParser {
16
48
  readonly input: string;
17
- private tokens;
49
+ private readonly tokens;
50
+ private tokenCursor;
51
+ private scanCursor;
52
+ private scanState;
18
53
  protected parseCursor: number;
19
- private lookAhead?;
20
- private tokenMap;
54
+ private readonly tokenMap;
21
55
  /**
22
- * The token map is tested in order. Return TinyToken
23
- * is {type: tokenMapKey, text: matchingText }, except
24
- * for the special tokenMapKeys:
25
- * * space: skipped and never returned
26
- * * char: matched string return in both .type and .text
27
- * * q*: any token name starting with 'q' is assumed to be
28
- * a quoted string and the text will have the first and
29
- * last characters stripped
56
+ * The token map is tested in order and the first matching rule wins.
30
57
  */
31
58
  constructor(input: string, tokenMap?: Record<string, RegExp>);
32
59
  parseError(str: string): TinyParseError;
33
60
  peek(): TinyToken;
34
- private getNext;
61
+ read(): TinyToken;
62
+ eof(): boolean;
63
+ private peekAt;
35
64
  /**
36
65
  * Return next token, if any token types are passed, read and require those
37
66
  * tokens, then return the last one.
38
67
  * @param types list of token types
39
68
  * @returns The last token read
40
69
  */
41
- next(...types: string[]): TinyToken;
42
- nextText(...texts: string[]): TinyToken;
70
+ expect(...types: string[]): TinyToken;
71
+ expectText(...texts: string[]): TinyToken;
72
+ match(...types: string[]): TinyToken | undefined;
73
+ matchText(...texts: string[]): TinyToken | undefined;
43
74
  skipTo(type: string): void;
44
75
  dump(): TinyToken[];
45
- private tokenize;
76
+ private fillBufferTo;
77
+ private consume;
78
+ private readNextToken;
79
+ private scanToken;
80
+ private eofToken;
46
81
  }