Line data Source code
1 : import * as arrow from 'apache-arrow';
2 :
3 : import { quoteIdent, quoteStringLiteral } from './sqlframe_builder.js';
4 : import { ColumnGroup, LIST_COLUMN, ORDINAL_COLUMN, ROWNUMBER_COLUMN, SKIPPED_COLUMN, STRING_COLUMN } from '../computation_types.js';
5 :
6 : /// The row number of a matching row in the result of buildDataSearchSQL()
7 1 : export const DATA_SEARCH_ROW_FIELD = 'row_idx';
8 :
9 : export interface SearchableResultColumn {
10 : /// Display-column index among original result columns, excluding the row-number column.
11 : columnIdx: number;
12 : /// Index into `TableComputationState.columnGroups`.
13 : columnGroupIdx: number;
14 : /// Arrow field name of the original result column.
15 : fieldName: string;
16 : }
17 :
18 1 : export function collectSearchableResultColumns(columnGroups: ColumnGroup[]): SearchableResultColumn[] {
19 1 : const columns: SearchableResultColumn[] = [];
20 1 : for (let columnGroupIdx = 0; columnGroupIdx < columnGroups.length; ++columnGroupIdx) {
21 1 : const columnGroup = columnGroups[columnGroupIdx];
22 1 : switch (columnGroup.type) {
23 1 : case ROWNUMBER_COLUMN:
24 1 : case SKIPPED_COLUMN:
25 1 : break;
26 1 : case ORDINAL_COLUMN:
27 1 : case STRING_COLUMN:
28 1 : case LIST_COLUMN:
29 1 : columns.push({
30 1 : columnIdx: columns.length,
31 1 : columnGroupIdx,
32 1 : fieldName: columnGroup.value.inputFieldName,
33 1 : });
34 : break;
35 1 : }
36 1 : }
37 1 : return columns;
38 1 : }
39 :
40 1 : export function buildColumnSearchSQL(columns: SearchableResultColumn[], pattern: string): string | null {
41 1 : if (columns.length === 0) {
42 1 : return null;
43 : }
44 1 : const values = columns.map(column =>
45 1 : `(${column.columnIdx}, ${column.columnGroupIdx}, ${quoteStringLiteral(column.fieldName)})`
46 : );
47 1 : return (
48 1 : `SELECT column_idx, column_group_idx\n` +
49 : `FROM (\n` +
50 : ` VALUES\n` +
51 1 : ` ${values.join(',\n ')}\n` +
52 : `) AS columns(column_idx, column_group_idx, name)\n` +
53 1 : `WHERE name ILIKE ${quoteStringLiteral(`%${pattern}%`)}\n` +
54 : `ORDER BY column_idx`
55 : );
56 1 : }
57 :
58 1 : export function buildDataSearchSQL(
59 1 : tableName: string,
60 1 : rowNumberFieldName: string,
61 1 : columns: SearchableResultColumn[],
62 1 : pattern: string,
63 1 : ): string {
64 1 : if (columns.length === 0) {
65 1 : const quotedRowNumber = quoteIdent(rowNumberFieldName);
66 1 : return (
67 1 : `SELECT\n` +
68 1 : ` ${quotedRowNumber} AS row_idx,\n` +
69 : ` array_agg(0 ORDER BY 0) AS column_indices\n` +
70 1 : `FROM ${quoteIdent(tableName)}\n` +
71 : `WHERE FALSE\n` +
72 1 : `GROUP BY ${quotedRowNumber}\n` +
73 : `ORDER BY row_idx`
74 : );
75 1 : }
76 1 : const quotedPattern = quoteStringLiteral(`%${pattern}%`);
77 1 : const quotedRowNumber = quoteIdent(rowNumberFieldName);
78 1 : const quotedTable = quoteIdent(tableName);
79 1 : const unions = columns.map(column => (
80 1 : ` SELECT ${quotedRowNumber} AS row_idx, ${column.columnIdx} AS column_idx\n` +
81 1 : ` FROM ${quotedTable}\n` +
82 1 : ` WHERE CAST(${quoteIdent(column.fieldName)} AS TEXT) ILIKE ${quotedPattern}`
83 : ));
84 1 : return (
85 1 : `WITH search AS (\n` +
86 1 : `${unions.join('\n UNION ALL\n')}\n` +
87 : `)\n` +
88 : `SELECT\n` +
89 : ` row_idx,\n` +
90 : ` array_agg(column_idx ORDER BY column_idx) AS column_indices\n` +
91 : `FROM search\n` +
92 : `GROUP BY row_idx\n` +
93 : `ORDER BY row_idx`
94 : );
95 1 : }
96 :
97 1 : export function parseColumnSearchMatches(table: arrow.Table): number[] {
98 1 : const groupColumn = table.getChild('column_group_idx') ?? table.getChildAt(1);
99 1 : if (groupColumn == null) {
100 0 : return [];
101 : }
102 1 : const matches: number[] = [];
103 1 : for (let i = 0; i < groupColumn.length; ++i) {
104 1 : matches.push(Number(groupColumn.get(i)));
105 : }
106 1 : return matches;
107 1 : }
108 :
109 1 : export function parseDataSearchMatches(table: arrow.Table): Map<number, number[]> {
110 1 : const rowColumn = table.getChild(DATA_SEARCH_ROW_FIELD) ?? table.getChildAt(0);
111 1 : const matchColumn = table.getChild('column_indices') ?? table.getChildAt(1);
112 1 : const matches = new Map<number, number[]>();
113 1 : if (rowColumn == null || matchColumn == null) {
114 0 : return matches;
115 : }
116 1 : for (let i = 0; i < rowColumn.length; ++i) {
117 1 : const rowIdx = Number(rowColumn.get(i));
118 1 : const rawMatches = matchColumn.get(i);
119 1 : const columnIndices = rawMatches == null
120 0 : ? []
121 1 : : Array.from(rawMatches as Iterable<unknown>, value => Number(value));
122 1 : matches.set(rowIdx, columnIndices);
123 1 : }
124 1 : return matches;
125 1 : }
|