@malloydata/malloy 0.0.222-dev241212001744 → 0.0.222-dev241212154316
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/dialect/duckdb/duckdb.d.ts +1 -0
- package/dist/dialect/duckdb/duckdb.js +124 -0
- package/dist/dialect/tiny_parser.d.ts +6 -2
- package/dist/dialect/tiny_parser.js +31 -4
- package/dist/lang/test/composite-field-usage.spec.js +24 -0
- package/dist/model/composite_source_utils.js +5 -1
- package/package.json +1 -1
|
@@ -55,6 +55,7 @@ export declare class DuckDBDialect extends PostgresBase {
|
|
|
55
55
|
[name: string]: DialectFunctionOverloadDef[];
|
|
56
56
|
};
|
|
57
57
|
malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
|
|
58
|
+
parseDuckDBType(sqlType: string): AtomicTypeDef;
|
|
58
59
|
sqlTypeToMalloyType(sqlType: string): LeafAtomicTypeDef;
|
|
59
60
|
castToString(expression: string): string;
|
|
60
61
|
concat(...values: string[]): string;
|
|
@@ -30,6 +30,7 @@ const dialect_1 = require("../dialect");
|
|
|
30
30
|
const pg_impl_1 = require("../pg_impl");
|
|
31
31
|
const dialect_functions_1 = require("./dialect_functions");
|
|
32
32
|
const function_overrides_1 = require("./function_overrides");
|
|
33
|
+
const tiny_parser_1 = require("../tiny_parser");
|
|
33
34
|
// need to refactor runSQL to take a SQLBlock instead of just a sql string.
|
|
34
35
|
const hackSplitComment = '-- hack: split on this';
|
|
35
36
|
const duckDBToMalloyTypes = {
|
|
@@ -287,6 +288,20 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
|
|
|
287
288
|
}
|
|
288
289
|
return malloyType.type;
|
|
289
290
|
}
|
|
291
|
+
parseDuckDBType(sqlType) {
|
|
292
|
+
const parser = new DuckDBTypeParser(sqlType);
|
|
293
|
+
try {
|
|
294
|
+
return parser.typeDef();
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
if (e instanceof tiny_parser_1.TinyParseError) {
|
|
298
|
+
return { type: 'sql native', rawType: sqlType };
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
290
305
|
sqlTypeToMalloyType(sqlType) {
|
|
291
306
|
var _a, _b, _c;
|
|
292
307
|
// Remove decimal precision
|
|
@@ -350,4 +365,113 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
|
|
|
350
365
|
}
|
|
351
366
|
}
|
|
352
367
|
exports.DuckDBDialect = DuckDBDialect;
|
|
368
|
+
class DuckDBTypeParser extends tiny_parser_1.TinyParser {
|
|
369
|
+
constructor(input) {
|
|
370
|
+
super(input, {
|
|
371
|
+
/* whitespace */ space: /^\s+/,
|
|
372
|
+
/* single quoted string */ qsingle: /^'([^']|'')*'/,
|
|
373
|
+
/* double quoted string */ qdouble: /^"([^"]|"")*"/,
|
|
374
|
+
/* (n) size */ size: /^\(\d+\)/,
|
|
375
|
+
/* (n1,n2) precision */ precision: /^\(\d+,\d+\)/,
|
|
376
|
+
/* T[] -> array of T */ arrayOf: /^\[]/,
|
|
377
|
+
/* other punctuation */ char: /^[,:[\]()-]/,
|
|
378
|
+
/* unquoted word */ id: /^\w+/,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
unquoteName(token) {
|
|
382
|
+
if (token.type === 'qsingle') {
|
|
383
|
+
return token.text.replace("''", '');
|
|
384
|
+
}
|
|
385
|
+
else if (token.type === 'qdouble') {
|
|
386
|
+
return token.text.replace('""', '');
|
|
387
|
+
}
|
|
388
|
+
return token.text;
|
|
389
|
+
}
|
|
390
|
+
sqlID(token) {
|
|
391
|
+
return token.text.toUpperCase();
|
|
392
|
+
}
|
|
393
|
+
typeDef() {
|
|
394
|
+
const unknownStart = this.parseCursor;
|
|
395
|
+
const wantID = this.next('id');
|
|
396
|
+
const id = this.sqlID(wantID);
|
|
397
|
+
let baseType;
|
|
398
|
+
if (id === 'VARCHAR') {
|
|
399
|
+
if (this.peek().type === 'size') {
|
|
400
|
+
this.next();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if ((id === 'DECIMAL' || id === 'NUMERIC') &&
|
|
404
|
+
this.peek().type === 'precision') {
|
|
405
|
+
this.next();
|
|
406
|
+
baseType = { type: 'number', numberType: 'float' };
|
|
407
|
+
}
|
|
408
|
+
else if (id === 'TIMESTAMP') {
|
|
409
|
+
if (this.peek().text === 'WITH') {
|
|
410
|
+
this.nextText('WITH', 'TIME', 'ZONE');
|
|
411
|
+
}
|
|
412
|
+
baseType = { type: 'timestamp' };
|
|
413
|
+
}
|
|
414
|
+
else if (duckDBToMalloyTypes[id]) {
|
|
415
|
+
baseType = duckDBToMalloyTypes[id];
|
|
416
|
+
}
|
|
417
|
+
else if (id === 'STRUCT') {
|
|
418
|
+
this.next('(');
|
|
419
|
+
baseType = { type: 'record', fields: [] };
|
|
420
|
+
for (;;) {
|
|
421
|
+
const fieldName = this.next();
|
|
422
|
+
if (fieldName.type === 'qsingle' ||
|
|
423
|
+
fieldName.type === 'qdouble' ||
|
|
424
|
+
fieldName.type === 'id') {
|
|
425
|
+
const fieldType = this.typeDef();
|
|
426
|
+
baseType.fields.push((0, malloy_types_1.mkFieldDef)(fieldType, this.unquoteName(fieldName), 'duckdb'));
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
if (fieldName.type !== ')') {
|
|
430
|
+
throw this.parseError('Expected identifier or ) to end STRUCT');
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
if (this.peek().type === ',') {
|
|
435
|
+
this.next();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
if (wantID.type === 'id') {
|
|
441
|
+
for (;;) {
|
|
442
|
+
const next = this.peek();
|
|
443
|
+
// Might be WEIRDTYP(a,b)[] ... stop at the []
|
|
444
|
+
if (next.type === 'arrayOf' || next.type === 'eof') {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
this.next();
|
|
448
|
+
}
|
|
449
|
+
baseType = {
|
|
450
|
+
type: 'sql native',
|
|
451
|
+
rawType: this.input.slice(unknownStart, this.parseCursor - unknownStart + 1),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
throw this.parseError('Could not understand type');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
while (this.peek().type === 'arrayOf') {
|
|
459
|
+
this.next();
|
|
460
|
+
if (baseType.type === 'record') {
|
|
461
|
+
baseType = {
|
|
462
|
+
type: 'array',
|
|
463
|
+
elementTypeDef: { type: 'record_element' },
|
|
464
|
+
fields: baseType.fields,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
baseType = {
|
|
469
|
+
type: 'array',
|
|
470
|
+
elementTypeDef: baseType,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return baseType;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
353
477
|
//# sourceMappingURL=duckdb.js.map
|
|
@@ -9,10 +9,12 @@ export interface TinyToken {
|
|
|
9
9
|
*
|
|
10
10
|
* NOTE: All parse errors are exceptions.
|
|
11
11
|
*/
|
|
12
|
+
export declare class TinyParseError extends Error {
|
|
13
|
+
}
|
|
12
14
|
export declare class TinyParser {
|
|
13
15
|
readonly input: string;
|
|
14
16
|
private tokens;
|
|
15
|
-
|
|
17
|
+
protected parseCursor: number;
|
|
16
18
|
private lookAhead?;
|
|
17
19
|
private tokenMap;
|
|
18
20
|
/**
|
|
@@ -26,7 +28,7 @@ export declare class TinyParser {
|
|
|
26
28
|
* last characters stripped
|
|
27
29
|
*/
|
|
28
30
|
constructor(input: string, tokenMap?: Record<string, RegExp>);
|
|
29
|
-
parseError(str: string):
|
|
31
|
+
parseError(str: string): TinyParseError;
|
|
30
32
|
peek(): TinyToken;
|
|
31
33
|
private getNext;
|
|
32
34
|
/**
|
|
@@ -36,6 +38,8 @@ export declare class TinyParser {
|
|
|
36
38
|
* @returns The last token read
|
|
37
39
|
*/
|
|
38
40
|
next(...types: string[]): TinyToken;
|
|
41
|
+
nextText(...texts: string[]): TinyToken;
|
|
39
42
|
skipTo(type: string): void;
|
|
43
|
+
dump(): TinyToken[];
|
|
40
44
|
private tokenize;
|
|
41
45
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.TinyParser = void 0;
|
|
9
|
+
exports.TinyParser = exports.TinyParseError = void 0;
|
|
10
10
|
/**
|
|
11
11
|
* Simple framework for writing schema parsers. The parsers using this felt
|
|
12
12
|
* better than the more ad-hoc code they replaced, and are smaller than
|
|
@@ -14,6 +14,9 @@ exports.TinyParser = void 0;
|
|
|
14
14
|
*
|
|
15
15
|
* NOTE: All parse errors are exceptions.
|
|
16
16
|
*/
|
|
17
|
+
class TinyParseError extends Error {
|
|
18
|
+
}
|
|
19
|
+
exports.TinyParseError = TinyParseError;
|
|
17
20
|
class TinyParser {
|
|
18
21
|
/**
|
|
19
22
|
* The token map is tested in order. Return TinyToken
|
|
@@ -28,19 +31,19 @@ class TinyParser {
|
|
|
28
31
|
constructor(input, tokenMap) {
|
|
29
32
|
this.input = input;
|
|
30
33
|
this.parseCursor = 0;
|
|
31
|
-
this.tokens = this.tokenize(input);
|
|
32
34
|
this.tokenMap = tokenMap !== null && tokenMap !== void 0 ? tokenMap : {
|
|
33
35
|
space: /^\s+/,
|
|
34
36
|
char: /^[,:[\]()-]/,
|
|
35
37
|
id: /^\w+/,
|
|
36
38
|
qstr: /^"\w+"/,
|
|
37
39
|
};
|
|
40
|
+
this.tokens = this.tokenize(input);
|
|
38
41
|
}
|
|
39
42
|
parseError(str) {
|
|
40
43
|
const errText = `INTERNAL ERROR parsing schema: ${str}\n` +
|
|
41
44
|
`${this.input}\n` +
|
|
42
45
|
`${' '.repeat(this.parseCursor)}^`;
|
|
43
|
-
return new
|
|
46
|
+
return new TinyParseError(errText);
|
|
44
47
|
}
|
|
45
48
|
peek() {
|
|
46
49
|
if (this.lookAhead) {
|
|
@@ -80,7 +83,24 @@ class TinyParser {
|
|
|
80
83
|
}
|
|
81
84
|
if (next)
|
|
82
85
|
return next;
|
|
83
|
-
throw this.parseError(`Expected ${expected}`);
|
|
86
|
+
throw this.parseError(`Expected token type '${expected}'`);
|
|
87
|
+
}
|
|
88
|
+
nextText(...texts) {
|
|
89
|
+
if (texts.length === 0)
|
|
90
|
+
return this.getNext();
|
|
91
|
+
let next = undefined;
|
|
92
|
+
let expected = texts[0];
|
|
93
|
+
for (const txt of texts) {
|
|
94
|
+
next = this.getNext();
|
|
95
|
+
expected = txt;
|
|
96
|
+
if (next.text !== txt) {
|
|
97
|
+
next = undefined;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (next)
|
|
102
|
+
return next;
|
|
103
|
+
throw this.parseError(`Expected '${expected}'`);
|
|
84
104
|
}
|
|
85
105
|
skipTo(type) {
|
|
86
106
|
for (;;) {
|
|
@@ -93,6 +113,12 @@ class TinyParser {
|
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
}
|
|
116
|
+
dump() {
|
|
117
|
+
const p = this.parseCursor;
|
|
118
|
+
const parts = [...this.tokenize(this.input)];
|
|
119
|
+
this.parseCursor = p;
|
|
120
|
+
return parts;
|
|
121
|
+
}
|
|
96
122
|
*tokenize(src) {
|
|
97
123
|
const tokenList = this.tokenMap;
|
|
98
124
|
while (this.parseCursor < src.length) {
|
|
@@ -112,6 +138,7 @@ class TinyParser {
|
|
|
112
138
|
type: tokenType === 'char' ? tokenText : tokenType,
|
|
113
139
|
text: tokenText,
|
|
114
140
|
};
|
|
141
|
+
break;
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
144
|
}
|
|
@@ -160,6 +160,30 @@ describe('composite sources', () => {
|
|
|
160
160
|
run: foo(param is 2) -> { group_by: y }
|
|
161
161
|
`).toTranslate();
|
|
162
162
|
});
|
|
163
|
+
test('array.each is okay', () => {
|
|
164
|
+
expect(`
|
|
165
|
+
##! experimental { composite_sources }
|
|
166
|
+
source: foo is compose(
|
|
167
|
+
a extend { dimension: x is 1 },
|
|
168
|
+
a extend { dimension: y is 2 }
|
|
169
|
+
) extend {
|
|
170
|
+
dimension: arr is [1, 2, 3]
|
|
171
|
+
}
|
|
172
|
+
run: foo -> { group_by: y, arr.each }
|
|
173
|
+
`).toTranslate();
|
|
174
|
+
});
|
|
175
|
+
test('timevalue extract okay', () => {
|
|
176
|
+
expect(`
|
|
177
|
+
##! experimental { composite_sources }
|
|
178
|
+
source: foo is compose(
|
|
179
|
+
a extend { dimension: x is 1 },
|
|
180
|
+
a extend { dimension: y is 2 }
|
|
181
|
+
) extend {
|
|
182
|
+
dimension: time is now
|
|
183
|
+
}
|
|
184
|
+
run: foo -> { group_by: y, time.day }
|
|
185
|
+
`).toTranslate();
|
|
186
|
+
});
|
|
163
187
|
});
|
|
164
188
|
});
|
|
165
189
|
//# sourceMappingURL=composite-field-usage.spec.js.map
|
|
@@ -99,11 +99,15 @@ function _resolveCompositeSources(path, source, compositeFieldUsage, narrowedCom
|
|
|
99
99
|
error: { code: 'composite_source_not_defined', data: { path: newPath } },
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
if (!(0, malloy_types_1.isJoined)(join)
|
|
102
|
+
if (!(0, malloy_types_1.isJoined)(join)) {
|
|
103
103
|
return {
|
|
104
104
|
error: { code: 'composite_source_not_a_join', data: { path: newPath } },
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
+
else if (!(0, malloy_types_1.isSourceDef)(join)) {
|
|
108
|
+
// Non-source join, like an array, skip it (no need to resolve)
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
107
111
|
const resolved = _resolveCompositeSources(newPath, join, joinedUsage, narrowedJoinedSources[joinName]);
|
|
108
112
|
if ('error' in resolved) {
|
|
109
113
|
return resolved;
|