@leuduan/block 0.0.0-stage → 2.0.0

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/pipe.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { type LineOrStr } from './line.js';
2
+ import { Pipe, TableExpression, type QueryExpression, type MapOrRecord } from './sql.js';
3
+ export declare function aggregate(aggregates: readonly LineOrStr[], groupBy?: readonly LineOrStr[]): Pipe;
4
+ export declare function as(alias: string): Pipe;
5
+ export declare function comment(text: string): Pipe;
6
+ /** COUNT(*) AS count aggregate pipe. */
7
+ export declare function count(): Pipe;
8
+ export declare function distinct(): Pipe;
9
+ export declare function drop(columns: readonly string[]): Pipe;
10
+ export declare function extend(exprs: readonly LineOrStr[], windows?: MapOrRecord<LineOrStr>): Pipe;
11
+ /** `on` and `using` are the whole clause, keyword included; a join takes at most one of them. */
12
+ export type JoinOptions = {
13
+ alias?: string;
14
+ } & ({
15
+ on?: LineOrStr;
16
+ using?: never;
17
+ } | {
18
+ using?: LineOrStr;
19
+ on?: never;
20
+ });
21
+ export declare function join(joinOp: LineOrStr, fromItem: TableExpression, options?: JoinOptions): Pipe;
22
+ export declare function limit(lim: number, offset?: number): Pipe;
23
+ export declare function orderBy(orders: readonly LineOrStr[]): Pipe;
24
+ export declare function raw(content: LineOrStr): Pipe;
25
+ export declare function rename(renames: MapOrRecord<string>): Pipe;
26
+ export declare function select(expressions: readonly LineOrStr[], windows?: MapOrRecord<LineOrStr>): Pipe;
27
+ export declare function set(exprs: readonly LineOrStr[]): Pipe;
28
+ export declare function union({ unionType, queries, }: {
29
+ unionType?: string;
30
+ queries: readonly QueryExpression[];
31
+ }): Pipe;
32
+ export declare function where(condition: LineOrStr | LineOrStr[]): Pipe;
33
+ export declare function with_(ctes: MapOrRecord<QueryExpression>): Pipe;
34
+ /** |> [CROSS|LEFT] JOIN UNNEST(expr) [AS alias] */
35
+ export declare function unnest(expr: TableExpression | LineOrStr, { alias, left }?: {
36
+ alias?: string;
37
+ left?: boolean;
38
+ }): Pipe;
39
+ //# sourceMappingURL=pipe.d.ts.map
package/dist/pipe.js ADDED
@@ -0,0 +1,106 @@
1
+ // Port of py/dbox/dbox/block/pipe.py
2
+ // GoogleSQL pipe steps: each factory builds its line directly.
3
+ import * as l from './line.js';
4
+ import {} from './line.js';
5
+ import { and, call as eCall, star as eStar } from './expr.js';
6
+ import * as h from './helpers.js';
7
+ import { Pipe, TableExpression, mapEntries } from './sql.js';
8
+ /** Mirrors Python textwrap.indent: prefix non-whitespace-only lines. */
9
+ function textwrapIndent(text, prefix) {
10
+ return text
11
+ .split('\n')
12
+ .map((line) => (/\S/.test(line) ? prefix + line : line))
13
+ .join('\n');
14
+ }
15
+ function listWithWindows(keyword, expressions, windows) {
16
+ const hasWindows = mapEntries(windows).length > 0;
17
+ return new Pipe(l.indentNext([
18
+ l.lit(keyword),
19
+ l.stackComma(expressions.map((e) => l.ensure(e))),
20
+ hasWindows ? h.windows(windows) : null,
21
+ ]));
22
+ }
23
+ export function aggregate(aggregates, groupBy = []) {
24
+ const groupingExpr = groupBy.length > 0 ? l.stackComma(groupBy.map((g) => l.ensure(g))) : null;
25
+ return new Pipe(l.stack([
26
+ l.indentNext([l.lit('|> AGGREGATE'), l.stackComma(aggregates.map((a) => l.ensure(a)))]),
27
+ groupingExpr ? l.indentNext([l.lit('GROUP BY'), groupingExpr]) : null,
28
+ ]));
29
+ }
30
+ export function as(alias) {
31
+ return new Pipe(l.indentNext([l.keyword('|> AS'), l.lit(alias)]));
32
+ }
33
+ export function comment(text) {
34
+ return raw(textwrapIndent(text, '-- '));
35
+ }
36
+ /** COUNT(*) AS count aggregate pipe. */
37
+ export function count() {
38
+ return aggregate([h.expression(eCall('COUNT', [eStar()]), 'count')]);
39
+ }
40
+ export function distinct() {
41
+ return new Pipe(l.keyword('|> DISTINCT'));
42
+ }
43
+ export function drop(columns) {
44
+ return new Pipe(l.indentNext([l.lit('|> DROP'), l.stackComma(columns.map((c) => l.lit(c)))]));
45
+ }
46
+ export function extend(exprs, windows = {}) {
47
+ return listWithWindows('|> EXTEND', exprs, windows);
48
+ }
49
+ export function join(joinOp, fromItem, options = {}) {
50
+ const { alias, on, using } = options;
51
+ if (!(fromItem instanceof TableExpression)) {
52
+ throw new Error('p.join: fromItem must be a TableExpression');
53
+ }
54
+ if (on !== undefined && using !== undefined) {
55
+ throw new Error('p.join cannot have both ON and USING clause.');
56
+ }
57
+ const joinLine = l.stack([l.lit('|>'), l.ensure(joinOp)]);
58
+ return new Pipe(l.indentNext([
59
+ l.indentNext([
60
+ joinLine,
61
+ fromItem.toLine(),
62
+ alias !== undefined ? l.stack([l.keyword('AS'), l.lit(alias)]) : null,
63
+ ]),
64
+ on !== undefined ? l.ensure(on) : null,
65
+ using !== undefined ? l.ensure(using) : null,
66
+ ]));
67
+ }
68
+ export function limit(lim, offset) {
69
+ const parts = [l.lit('|> LIMIT'), l.lit(String(lim))];
70
+ if (offset !== undefined)
71
+ parts.push(l.lit('OFFSET'), l.lit(String(offset)));
72
+ return new Pipe(l.indentNext(parts));
73
+ }
74
+ export function orderBy(orders) {
75
+ return new Pipe(l.indentNext([l.lit('|> ORDER BY'), l.stackComma(orders.map((o) => l.ensure(o)))]));
76
+ }
77
+ export function raw(content) {
78
+ return new Pipe(l.ensure(content));
79
+ }
80
+ export function rename(renames) {
81
+ const renameLines = l.stackComma(mapEntries(renames).map(([old, next]) => l.lit(`${old} AS ${next}`)));
82
+ return new Pipe(l.indentNext([l.lit('|> RENAME'), renameLines]));
83
+ }
84
+ export function select(expressions, windows = {}) {
85
+ return listWithWindows('|> SELECT', expressions, windows);
86
+ }
87
+ export function set(exprs) {
88
+ return new Pipe(l.indentNext([l.lit('|> SET'), l.stackComma(exprs.map((e) => l.ensure(e)))]));
89
+ }
90
+ export function union({ unionType = 'all', queries, }) {
91
+ const unionLine = l.keyword(unionType === 'all' ? '|> UNION ALL' : `|> ${unionType.toUpperCase()}`);
92
+ const queryLines = queries.map((query) => l.paren(query.toLine()));
93
+ return new Pipe(l.indentNext([unionLine, l.stackComma(queryLines)]));
94
+ }
95
+ export function where(condition) {
96
+ const cond = Array.isArray(condition) ? and(condition.map((c) => l.ensure(c))) : l.ensure(condition);
97
+ return new Pipe(l.indentNext([l.lit('|> WHERE'), cond]));
98
+ }
99
+ export function with_(ctes) {
100
+ return new Pipe(h.ctes(ctes, { withPipe: true }));
101
+ }
102
+ /** |> [CROSS|LEFT] JOIN UNNEST(expr) [AS alias] */
103
+ export function unnest(expr, { alias, left = false } = {}) {
104
+ const source = expr instanceof TableExpression ? expr.toLine() : l.ensure(expr);
105
+ return join(l.keyword(left ? 'LEFT JOIN' : 'CROSS JOIN'), new TableExpression(h.unnest(source)), { alias });
106
+ }
@@ -0,0 +1,7 @@
1
+ export interface Problem {
2
+ message: string;
3
+ /** The dataset cannot be built until this is fixed. */
4
+ blocks: boolean;
5
+ }
6
+ export declare function problemsOf(def: unknown): Problem[];
7
+ //# sourceMappingURL=problems.d.ts.map
@@ -0,0 +1,128 @@
1
+ // What a definition must declare, judged on the object `define(...)` received, so it does not trust the types. The
2
+ // builder refuses a dataset with a blocking problem; callers report the rest as they see fit.
3
+ import { PARAM_TYPES } from './descriptor.js';
4
+ const PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
+ const TABLE_NAME = /^[\p{L}\p{M}\p{N}_-]{1,1024}$/u;
6
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
7
+ const TIME = /^\d{2}:\d{2}:\d{2}(\.\d+)?$/;
8
+ const DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?$/;
9
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}(:?\d{2})?)$/;
10
+ function validValue(type, value) {
11
+ switch (type) {
12
+ case 'STRING':
13
+ return typeof value === 'string';
14
+ case 'INT64':
15
+ return typeof value === 'number' && Number.isSafeInteger(value);
16
+ case 'FLOAT64':
17
+ return typeof value === 'number' && Number.isFinite(value);
18
+ case 'BOOL':
19
+ return typeof value === 'boolean';
20
+ case 'DATE':
21
+ return typeof value === 'string' && DATE.test(value) && !Number.isNaN(Date.parse(value));
22
+ case 'TIME':
23
+ return typeof value === 'string' && TIME.test(value);
24
+ case 'DATETIME':
25
+ return typeof value === 'string' && DATETIME.test(value);
26
+ case 'TIMESTAMP':
27
+ return typeof value === 'string' && TIMESTAMP.test(value);
28
+ }
29
+ }
30
+ function isRecord(value) {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
32
+ }
33
+ function nonEmpty(value) {
34
+ return typeof value === 'string' && value.trim() !== '';
35
+ }
36
+ function notes(field, value, out) {
37
+ if (value === undefined)
38
+ return;
39
+ if (!isRecord(value) || !Object.values(value).every((note) => typeof note === 'string')) {
40
+ out.push({ message: `\`${field}\` is a record of name → note (string)`, blocks: false });
41
+ }
42
+ }
43
+ export function problemsOf(def) {
44
+ if (!isRecord(def) || (def.kind !== 'table' && def.kind !== 'query')) {
45
+ return [{ message: "define takes an object with kind 'table' or 'query'", blocks: true }];
46
+ }
47
+ const out = [];
48
+ const flag = (message, blocks = false) => out.push({ message, blocks });
49
+ if (!nonEmpty(def.description))
50
+ flag('`description` is missing or empty');
51
+ if (!nonEmpty(def.grain))
52
+ flag('`grain` is missing or empty');
53
+ notes('columns', def.columns, out);
54
+ notes('relations', def.relations, out);
55
+ if (def.kind === 'table') {
56
+ for (const field of ['build', 'params', 'cases', 'role']) {
57
+ if (def[field] !== undefined)
58
+ flag(`\`${field}\` belongs to queries, not tables`, true);
59
+ }
60
+ if (def.table !== undefined && !(typeof def.table === 'string' && TABLE_NAME.test(def.table))) {
61
+ flag('`table` is not a valid table name', true);
62
+ }
63
+ if (def.partitionedBy !== undefined && !nonEmpty(def.partitionedBy))
64
+ flag('`partitionedBy` is a column name');
65
+ const clustered = def.clusteredBy;
66
+ if (clustered !== undefined && !(Array.isArray(clustered) && clustered.every(nonEmpty))) {
67
+ flag('`clusteredBy` is a list of column names');
68
+ }
69
+ return out;
70
+ }
71
+ for (const field of ['table', 'partitionedBy', 'clusteredBy']) {
72
+ if (def[field] !== undefined)
73
+ flag(`\`${field}\` belongs to tables, not queries`, true);
74
+ }
75
+ if (typeof def.build !== 'function')
76
+ flag('`build` is missing', true);
77
+ if (def.role !== undefined && def.role !== 'metric')
78
+ flag("`role` can only be 'metric'");
79
+ const params = def.params === undefined ? {} : def.params;
80
+ const declared = new Map();
81
+ if (!isRecord(params)) {
82
+ flag('`params` is a record of name → { type, default? }', true);
83
+ }
84
+ else {
85
+ for (const [name, spec] of Object.entries(params)) {
86
+ if (!PARAM_NAME.test(name))
87
+ flag(`parameter "${name}" is not a valid name`, true);
88
+ if (!isRecord(spec) || !PARAM_TYPES.includes(spec.type)) {
89
+ flag(`parameter "${name}" needs a type (${PARAM_TYPES.join(', ')})`, true);
90
+ continue;
91
+ }
92
+ const type = spec.type;
93
+ if (spec.default !== undefined && !validValue(type, spec.default)) {
94
+ flag(`the default of parameter "${name}" is not a ${type}`, true);
95
+ }
96
+ declared.set(name, { type, hasDefault: spec.default !== undefined });
97
+ }
98
+ }
99
+ const cases = def.cases === undefined ? {} : def.cases;
100
+ if (!isRecord(cases)) {
101
+ flag('`cases` is a record of case name → parameter values', true);
102
+ return out;
103
+ }
104
+ const unbound = [...declared].filter(([, spec]) => !spec.hasDefault).map(([name]) => name);
105
+ if (unbound.length > 0 && Object.keys(cases).length === 0) {
106
+ flag(`parameter ${unbound.map((name) => `"${name}"`).join(', ')} has no default and the dataset declares no cases`, true);
107
+ }
108
+ for (const [caseName, bindings] of Object.entries(cases)) {
109
+ if (caseName === '')
110
+ flag('a case needs a name', true);
111
+ if (!isRecord(bindings)) {
112
+ flag(`case "${caseName}" is a record of parameter values`, true);
113
+ continue;
114
+ }
115
+ for (const [name, value] of Object.entries(bindings)) {
116
+ const spec = declared.get(name);
117
+ if (!spec)
118
+ flag(`case "${caseName}" binds "${name}", which is not a parameter`, true);
119
+ else if (!validValue(spec.type, value))
120
+ flag(`case "${caseName}" binds "${name}" to a value that is not a ${spec.type}`, true);
121
+ }
122
+ for (const name of unbound) {
123
+ if (!(name in bindings))
124
+ flag(`case "${caseName}" does not bind "${name}", which has no default`, true);
125
+ }
126
+ }
127
+ return out;
128
+ }
@@ -0,0 +1,37 @@
1
+ import * as l from './line.js';
2
+ import { type LineOrStr } from './line.js';
3
+ import { QueryExpression, GoogleSqlQuery, TableExpression, Pipe, type MapOrRecord } from './sql.js';
4
+ export declare function raw(sql: LineOrStr): QueryExpression;
5
+ export declare function named(name: string): QueryExpression;
6
+ /**
7
+ * A GoogleSqlQuery around `queryExpression`: optional WITH, then the conventional ORDER BY / LIMIT suffix lines,
8
+ * then the `|>` pipes. A query that is already a GoogleSqlQuery is nested in parentheses, not merged.
9
+ */
10
+ export declare function googleQuery({ ctes, queryExpression, orderBy, limitOffset, pipes, }: {
11
+ ctes?: MapOrRecord<QueryExpression>;
12
+ queryExpression: QueryExpression;
13
+ orderBy?: l.Line;
14
+ limitOffset?: l.Line;
15
+ pipes?: readonly Pipe[];
16
+ }): GoogleSqlQuery;
17
+ export declare function exceptQueries(...queries: QueryExpression[]): QueryExpression;
18
+ export declare function standardSelect({ distinct, selectList, tableExpr, whereClause, groupBy, having, qualify, windows, ctes, }: {
19
+ distinct?: boolean;
20
+ selectList: readonly LineOrStr[];
21
+ tableExpr?: TableExpression;
22
+ whereClause?: LineOrStr;
23
+ groupBy?: LineOrStr;
24
+ having?: LineOrStr;
25
+ qualify?: LineOrStr;
26
+ windows?: MapOrRecord<LineOrStr>;
27
+ ctes?: MapOrRecord<QueryExpression>;
28
+ }): QueryExpression;
29
+ export declare function combineQueries(queries: MapOrRecord<QueryExpression>, marker?: string): QueryExpression;
30
+ export declare function stackQueries(queries: QueryExpression[], opts: {
31
+ keys: string[];
32
+ hashColumns: string[];
33
+ }): QueryExpression;
34
+ type DictRow = Record<string, LineOrStr | number> | Map<string, LineOrStr | number>;
35
+ export declare function data(schemaFields: readonly LineOrStr[] | MapOrRecord<LineOrStr>, rows: readonly LineOrStr[] | DictRow[]): QueryExpression;
36
+ export {};
37
+ //# sourceMappingURL=query.d.ts.map
package/dist/query.js ADDED
@@ -0,0 +1,164 @@
1
+ // Port of py/dbox/dbox/block/query.py
2
+ // GoogleSQL query builders.
3
+ import * as l from './line.js';
4
+ import {} from './line.js';
5
+ import * as e from './expr.js';
6
+ import * as h from './helpers.js';
7
+ import * as p from './pipe.js';
8
+ import { QueryExpression, GoogleSqlQuery, RawQuery, NamedQueryExpression, TableExpression, Pipe, mapEntries, pipe, } from './sql.js';
9
+ // ---------------------------------------------------------------------------
10
+ // SelectQuery (Python query.py:46-103): the standard SELECT form
11
+ // ---------------------------------------------------------------------------
12
+ class SelectQuery extends QueryExpression {
13
+ ctes;
14
+ isDistinct;
15
+ selectList;
16
+ tableExpr;
17
+ whereClause;
18
+ groupBy;
19
+ having;
20
+ qualify;
21
+ windows;
22
+ constructor(props) {
23
+ super();
24
+ this.isDistinct = props.distinct;
25
+ this.selectList = props.selectList;
26
+ this.tableExpr = props.tableExpr;
27
+ this.ctes = props.ctes;
28
+ this.whereClause = props.whereClause;
29
+ this.groupBy = props.groupBy;
30
+ this.having = props.having;
31
+ this.qualify = props.qualify;
32
+ this.windows = props.windows;
33
+ }
34
+ toLine() {
35
+ const selectKeyword = this.isDistinct ? l.stack([l.keyword('SELECT'), l.keyword('DISTINCT')]) : l.keyword('SELECT');
36
+ return l.stack([
37
+ this.ctes.size > 0 ? h.ctes(this.ctes) : null,
38
+ l.indentNext([selectKeyword, l.stackComma(this.selectList)]),
39
+ this.tableExpr ? l.indentNext([l.keyword('FROM'), this.tableExpr.toLine()]) : null,
40
+ this.whereClause ? l.indentNext([l.keyword('WHERE'), this.whereClause]) : null,
41
+ this.groupBy,
42
+ this.having ? l.indentNext([l.keyword('HAVING'), this.having]) : null,
43
+ this.qualify ? l.indentNext([l.keyword('QUALIFY'), this.qualify]) : null,
44
+ this.windows.size > 0 ? h.windows(this.windows) : null,
45
+ ]);
46
+ }
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Public query builders
50
+ // ---------------------------------------------------------------------------
51
+ export function raw(sql) {
52
+ return new RawQuery(l.ensure(sql));
53
+ }
54
+ export function named(name) {
55
+ return new NamedQueryExpression(name);
56
+ }
57
+ /**
58
+ * A GoogleSqlQuery around `queryExpression`: optional WITH, then the conventional ORDER BY / LIMIT suffix lines,
59
+ * then the `|>` pipes. A query that is already a GoogleSqlQuery is nested in parentheses, not merged.
60
+ */
61
+ export function googleQuery({ ctes = {}, queryExpression, orderBy, limitOffset, pipes = [], }) {
62
+ return new GoogleSqlQuery({
63
+ ctes: new Map(mapEntries(ctes)),
64
+ queryExpression,
65
+ orderByLine: orderBy,
66
+ limitOffset,
67
+ pipes,
68
+ });
69
+ }
70
+ export function exceptQueries(...queries) {
71
+ const lines = queries.map((q) => l.paren(q.toLine()));
72
+ const line = l.stackWithSep(lines, l.keyword('EXCEPT DISTINCT'));
73
+ return raw(line);
74
+ }
75
+ export function standardSelect({ distinct = false, selectList, tableExpr, whereClause, groupBy, having, qualify, windows = {}, ctes = {}, }) {
76
+ return new SelectQuery({
77
+ distinct,
78
+ selectList: selectList.map((s) => l.ensure(s)),
79
+ tableExpr,
80
+ ctes: new Map(mapEntries(ctes)),
81
+ whereClause: whereClause !== undefined ? l.ensure(whereClause) : undefined,
82
+ groupBy: groupBy !== undefined ? l.ensure(groupBy) : undefined,
83
+ having: having !== undefined ? l.ensure(having) : undefined,
84
+ qualify: qualify !== undefined ? l.ensure(qualify) : undefined,
85
+ windows: new Map(mapEntries(windows).map(([k, v]) => [k, l.ensure(v)])),
86
+ });
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // combineQueries (query.py:217-228)
90
+ // ---------------------------------------------------------------------------
91
+ export function combineQueries(queries, marker = '__combined__') {
92
+ const entries = mapEntries(queries);
93
+ if (entries.length === 0)
94
+ throw new Error('combineQueries requires at least one query');
95
+ const lMarker = l.lit(`1 AS ${marker}`);
96
+ let query = null;
97
+ for (const [name, queryExpr] of entries) {
98
+ if (query === null) {
99
+ query = pipe(queryExpr, p.extend([lMarker]), p.as(name), p.select([name]));
100
+ }
101
+ else {
102
+ query = pipe(query, p.union({
103
+ unionType: 'outer union all by name',
104
+ queries: [pipe(queryExpr, p.extend([lMarker]), p.as(name), p.select([name]))],
105
+ }));
106
+ }
107
+ }
108
+ return query;
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // stackQueries (query.py:231-268)
112
+ // ---------------------------------------------------------------------------
113
+ export function stackQueries(queries, opts) {
114
+ const { keys, hashColumns } = opts;
115
+ // Copy to avoid mutating caller's array (Python mutates in-place; we don't).
116
+ const qs = [...queries];
117
+ if (hashColumns && hashColumns.length > 0) {
118
+ for (let i = 0; i < qs.length; i++) {
119
+ const calcJsonValue = e.call('to_json_string', [e.struct(hashColumns)]);
120
+ const hashExpr = e.call('farm_fingerprint', [calcJsonValue]);
121
+ qs[i] = pipe(qs[i], p.extend([h.expression(hashExpr, '__hash')]));
122
+ }
123
+ }
124
+ for (let i = 0; i < qs.length; i++) {
125
+ qs[i] = pipe(qs[i], p.select([h.expression('*'), h.expression(`'${i}' as lineage_index`)]));
126
+ }
127
+ let query = pipe(qs[0], p.union({ unionType: 'outer union all by name', queries: qs.slice(1) }));
128
+ const fn = (q) => {
129
+ const selectValues = l.paren(h.selectAsStruct([`t.* except (${keys.join(', ')})`]));
130
+ return pipe(q, p.as('t'), p.select([...keys, h.expression(selectValues, 'values')]));
131
+ };
132
+ query = fn(query);
133
+ const calcRow = e.call('array_agg', [h.expression('values order by values.lineage_index')]);
134
+ query = pipe(query, p.aggregate([h.expression(calcRow, 'row'), h.expression('count(distinct values.__hash) > 1 or count(*) < 2', 'is_different')], keys));
135
+ return query;
136
+ }
137
+ function ensureData(v) {
138
+ if (typeof v === 'number')
139
+ return l.ensure(String(v));
140
+ return l.ensure(v);
141
+ }
142
+ export function data(schemaFields, rows) {
143
+ let rowLineOrStrs;
144
+ const isDict = !Array.isArray(schemaFields);
145
+ if (isDict) {
146
+ const schema = schemaFields;
147
+ const keys = mapEntries(schema).map(([k]) => k);
148
+ rowLineOrStrs = rows.map((row) => {
149
+ const get = (k) => row instanceof Map ? row.get(k) : row[k];
150
+ return l.paren(l.stackComma(keys.map((k) => ensureData(get(k)))), l.BLANK);
151
+ });
152
+ }
153
+ else {
154
+ rowLineOrStrs = [...rows];
155
+ }
156
+ const schemaForStruct = isDict ? schemaFields : schemaFields;
157
+ const rowLines = [
158
+ l.indentNext([h.structType(schemaForStruct), l.ensure(rowLineOrStrs[0])]),
159
+ ...rowLineOrStrs.slice(1).map((r) => l.ensure(r)),
160
+ ];
161
+ const source = h.array(rowLines);
162
+ const sqlLines = [l.keyword('select *'), l.indentNext([l.keyword('from'), h.unnest(source)])];
163
+ return raw(l.stack(sqlLines));
164
+ }
package/dist/sql.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { type Line, type Options } from './line.js';
2
+ export type MapOrRecord<V> = Map<string, V> | Record<string, V>;
3
+ export declare function mapEntries<V>(m: MapOrRecord<V>): [string, V][];
4
+ export declare abstract class SqlSegment {
5
+ abstract toLine(): Line;
6
+ format(options?: Options): string;
7
+ toString(): string;
8
+ }
9
+ /** One `|>` step. Built by the `p` factories. */
10
+ export declare class Pipe extends SqlSegment {
11
+ readonly line: Line;
12
+ constructor(line: Line);
13
+ toLine(): Line;
14
+ }
15
+ /** Something that goes after FROM or JOIN. Built by the `t` factories. */
16
+ export declare class TableExpression extends SqlSegment {
17
+ readonly line: Line;
18
+ constructor(line: Line);
19
+ toLine(): Line;
20
+ }
21
+ export declare abstract class QueryExpression extends SqlSegment {
22
+ /** Default toTableExpression wraps in a parenthesized subquery. */
23
+ toTableExpression(): TableExpression;
24
+ }
25
+ export declare class RawQuery extends QueryExpression {
26
+ readonly sql: Line;
27
+ constructor(sql: Line);
28
+ toLine(): Line;
29
+ }
30
+ export declare class NamedQueryExpression extends QueryExpression {
31
+ readonly name: string;
32
+ constructor(name: string);
33
+ toLine(): Line;
34
+ /** Named expressions refer to CTEs/tables — no subquery wrapping. */
35
+ toTableExpression(): TableExpression;
36
+ }
37
+ /** `FROM table`; a string is emitted as given, so quote identifiers with `t.path(id, true)` or `e.ident`. */
38
+ export declare function from(tableExpression: TableExpression | string): QueryExpression;
39
+ export interface GoogleSqlQueryProps {
40
+ ctes?: Map<string, QueryExpression>;
41
+ queryExpression: QueryExpression;
42
+ /** orderByLine avoids collision with the p.orderBy pipe. */
43
+ orderByLine?: Line;
44
+ limitOffset?: Line;
45
+ pipes?: readonly Pipe[];
46
+ }
47
+ export declare class GoogleSqlQuery extends QueryExpression {
48
+ readonly ctes: Map<string, QueryExpression>;
49
+ readonly queryExpression: QueryExpression;
50
+ readonly orderByLine: Line | undefined;
51
+ readonly limitOffset: Line | undefined;
52
+ readonly pipes: readonly Pipe[];
53
+ constructor({ ctes, queryExpression, orderByLine, limitOffset, pipes }: GoogleSqlQueryProps);
54
+ toLine(): Line;
55
+ }
56
+ /**
57
+ * Apply `|>` steps to a query. Pipe syntax is GoogleSQL's, so the result is a GoogleSqlQuery. A GoogleSqlQuery takes
58
+ * the steps on its own pipe list (only its outer wrapper is flattened); anything else is wrapped. The input is not
59
+ * changed.
60
+ */
61
+ export declare function pipe(query: QueryExpression, ...steps: Pipe[]): GoogleSqlQuery;
62
+ type SqlInterp = string | number | Line | SqlSegment;
63
+ /**
64
+ * Tagged template for raw SQL fragments.
65
+ * - string/number interpolations inline as raw SQL text (no quoting).
66
+ * - Line interpolations embed structurally (breaker recurses into them).
67
+ * - SqlSegment (QueryExpression etc.) interpolations use .toLine().
68
+ * For quoted identifiers use ident(name); for string literals use e.literalString(x).
69
+ */
70
+ export declare function sql(strings: TemplateStringsArray, ...values: SqlInterp[]): Line;
71
+ export {};
72
+ //# sourceMappingURL=sql.d.ts.map