@rozenite/sqlite-plugin 1.7.0-rc.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/CHANGELOG.md +8 -0
- package/LICENSE +20 -0
- package/README.md +102 -0
- package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/react-native/chunks/bridge-values.cjs +5 -0
- package/dist/react-native/chunks/bridge-values.js +258 -0
- package/dist/react-native/chunks/index.require.cjs +1 -0
- package/dist/react-native/chunks/index.require.js +118 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
- package/dist/react-native/index.cjs +1 -0
- package/dist/react-native/index.d.ts +178 -0
- package/dist/react-native/index.js +16 -0
- package/dist/rozenite.json +1 -0
- package/package.json +83 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +55 -0
- package/rozenite.config.ts +8 -0
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
- package/src/react-native/adapters/expo-sqlite.ts +230 -0
- package/src/react-native/adapters/generic.ts +88 -0
- package/src/react-native/adapters/index.ts +9 -0
- package/src/react-native/sqlite-view.ts +24 -0
- package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
- package/src/shared/__tests__/bridge-values.test.ts +34 -0
- package/src/shared/__tests__/sql.test.ts +55 -0
- package/src/shared/bridge-values.ts +170 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/sql.ts +420 -0
- package/src/shared/types.ts +81 -0
- package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
- package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
- package/src/ui/__tests__/value-utils.test.tsx +12 -0
- package/src/ui/cell-detail-drawer.tsx +65 -0
- package/src/ui/globals.css +1415 -0
- package/src/ui/panel.tsx +2815 -0
- package/src/ui/query-result-table.tsx +199 -0
- package/src/ui/sql-editor-utils.ts +352 -0
- package/src/ui/sql-editor.tsx +509 -0
- package/src/ui/sqlite-data-table.tsx +296 -0
- package/src/ui/sqlite-introspection.ts +189 -0
- package/src/ui/sqlite-modal-controls.tsx +32 -0
- package/src/ui/sqlite-row-delete-modal.tsx +130 -0
- package/src/ui/sqlite-row-edit-modal.tsx +487 -0
- package/src/ui/sqlite-row-edit-value.ts +53 -0
- package/src/ui/sqlite-row-mutations.ts +246 -0
- package/src/ui/sqlite-table-column-order.ts +154 -0
- package/src/ui/use-sqlite-requests.ts +205 -0
- package/src/ui/utils.ts +107 -0
- package/src/ui/value-utils.tsx +162 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
import type { CellContext, ColumnDef, OnChangeFn } from '@tanstack/react-table';
|
|
3
|
+
import type { SqliteQueryResult } from '../shared/types';
|
|
4
|
+
import { formatDuration, formatNumber } from './utils';
|
|
5
|
+
import {
|
|
6
|
+
getMetadataBadgeClassName,
|
|
7
|
+
getValueKind,
|
|
8
|
+
getValuePreview,
|
|
9
|
+
} from './value-utils';
|
|
10
|
+
import { CellDetailDrawer } from './cell-detail-drawer';
|
|
11
|
+
import { SqliteDataTable } from './sqlite-data-table';
|
|
12
|
+
|
|
13
|
+
type QueryResultTableProps = {
|
|
14
|
+
tableId: string;
|
|
15
|
+
result: SqliteQueryResult | null;
|
|
16
|
+
columnOrder: string[];
|
|
17
|
+
onColumnOrderChange: OnChangeFn<string[]>;
|
|
18
|
+
emptyTitle: string;
|
|
19
|
+
emptyDescription: string;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
showMetadata?: boolean;
|
|
22
|
+
tableClassName?: string;
|
|
23
|
+
shellClassName?: string;
|
|
24
|
+
scrollContainerClassName?: string;
|
|
25
|
+
rowNumberOffset?: number;
|
|
26
|
+
columnMeta?: Record<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
type?: string | null;
|
|
30
|
+
isPrimaryKey?: boolean;
|
|
31
|
+
isForeignKey?: boolean;
|
|
32
|
+
}
|
|
33
|
+
>;
|
|
34
|
+
hiddenColumnIds?: string[];
|
|
35
|
+
rowActions?: {
|
|
36
|
+
columnId: string;
|
|
37
|
+
header: string;
|
|
38
|
+
cell: (row: Record<string, unknown>, rowIndex: number) => ReactNode;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type DrawerPayload = {
|
|
43
|
+
title: string;
|
|
44
|
+
value: Record<string, unknown>;
|
|
45
|
+
} | null;
|
|
46
|
+
|
|
47
|
+
const joinClassNames = (
|
|
48
|
+
...classNames: Array<string | false | null | undefined>
|
|
49
|
+
) => classNames.filter(Boolean).join(' ');
|
|
50
|
+
|
|
51
|
+
const getColumnHeaderTitle = (
|
|
52
|
+
column: string,
|
|
53
|
+
meta?: {
|
|
54
|
+
type?: string | null;
|
|
55
|
+
isPrimaryKey?: boolean;
|
|
56
|
+
isForeignKey?: boolean;
|
|
57
|
+
},
|
|
58
|
+
) => {
|
|
59
|
+
const details = [
|
|
60
|
+
meta?.type,
|
|
61
|
+
meta?.isPrimaryKey ? 'PK' : null,
|
|
62
|
+
!meta?.isPrimaryKey && meta?.isForeignKey ? 'FK' : null,
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
|
|
65
|
+
return details.length > 0 ? `${column} (${details.join(', ')})` : column;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const QueryResultTable = ({
|
|
69
|
+
tableId,
|
|
70
|
+
result,
|
|
71
|
+
columnOrder,
|
|
72
|
+
onColumnOrderChange,
|
|
73
|
+
emptyTitle,
|
|
74
|
+
emptyDescription,
|
|
75
|
+
loading = false,
|
|
76
|
+
showMetadata = true,
|
|
77
|
+
tableClassName,
|
|
78
|
+
shellClassName,
|
|
79
|
+
scrollContainerClassName,
|
|
80
|
+
rowNumberOffset = 0,
|
|
81
|
+
columnMeta,
|
|
82
|
+
hiddenColumnIds = [],
|
|
83
|
+
rowActions,
|
|
84
|
+
}: QueryResultTableProps) => {
|
|
85
|
+
const [drawerPayload, setDrawerPayload] = useState<DrawerPayload>(null);
|
|
86
|
+
|
|
87
|
+
const columns = useMemo(() => result?.columns ?? [], [result]);
|
|
88
|
+
const rows = result?.rows ?? [];
|
|
89
|
+
const metadata = result?.metadata ?? null;
|
|
90
|
+
const visibleColumns = useMemo(
|
|
91
|
+
() => columns.filter((column) => !hiddenColumnIds.includes(column)),
|
|
92
|
+
[columns, hiddenColumnIds],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const handleInspectRow = (row: Record<string, unknown>, rowIndex: number) => {
|
|
96
|
+
setDrawerPayload({
|
|
97
|
+
title: `Row ${rowNumberOffset + rowIndex + 1}`,
|
|
98
|
+
value: Object.fromEntries(
|
|
99
|
+
visibleColumns.map((column) => [column, row[column]]),
|
|
100
|
+
),
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const tableColumns = useMemo<ColumnDef<Record<string, unknown>, unknown>[]>(
|
|
105
|
+
() => [
|
|
106
|
+
...visibleColumns.map((column) => ({
|
|
107
|
+
id: column,
|
|
108
|
+
header: () => (
|
|
109
|
+
<span title={getColumnHeaderTitle(column, columnMeta?.[column])}>
|
|
110
|
+
{column}
|
|
111
|
+
</span>
|
|
112
|
+
),
|
|
113
|
+
accessorFn: (row: Record<string, unknown>) => row[column],
|
|
114
|
+
cell: ({ row }: CellContext<Record<string, unknown>, unknown>) => {
|
|
115
|
+
const value = row.original[column];
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="sqlite-cell-value">
|
|
119
|
+
<span className="sqlite-cell-preview">
|
|
120
|
+
{getValuePreview(value)}
|
|
121
|
+
</span>
|
|
122
|
+
<span className="sqlite-cell-kind">{getValueKind(value)}</span>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
})),
|
|
127
|
+
...(rowActions
|
|
128
|
+
? [
|
|
129
|
+
{
|
|
130
|
+
id: rowActions.columnId,
|
|
131
|
+
header: rowActions.header,
|
|
132
|
+
enableResizing: false,
|
|
133
|
+
size: 112,
|
|
134
|
+
minSize: 112,
|
|
135
|
+
maxSize: 140,
|
|
136
|
+
cell: ({ row }) => rowActions.cell(row.original, row.index),
|
|
137
|
+
} satisfies ColumnDef<Record<string, unknown>, unknown>,
|
|
138
|
+
]
|
|
139
|
+
: []),
|
|
140
|
+
],
|
|
141
|
+
[columnMeta, rowActions, visibleColumns],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
{showMetadata && metadata ? (
|
|
147
|
+
<div className="sqlite-inline-metadata">
|
|
148
|
+
<span
|
|
149
|
+
className={joinClassNames(
|
|
150
|
+
'sqlite-badge',
|
|
151
|
+
getMetadataBadgeClassName(metadata),
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
{metadata.statementType}
|
|
155
|
+
</span>
|
|
156
|
+
<span className="sqlite-inline-stat sqlite-tabular">
|
|
157
|
+
{formatNumber(metadata.rowCount)} rows
|
|
158
|
+
</span>
|
|
159
|
+
<span className="sqlite-inline-stat sqlite-tabular">
|
|
160
|
+
{formatNumber(metadata.changes)} changes
|
|
161
|
+
</span>
|
|
162
|
+
<span className="sqlite-inline-stat sqlite-tabular">
|
|
163
|
+
last insert {formatNumber(metadata.lastInsertRowId)}
|
|
164
|
+
</span>
|
|
165
|
+
<span className="sqlite-inline-stat sqlite-tabular">
|
|
166
|
+
{formatDuration(metadata.durationMs)}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
|
|
171
|
+
<SqliteDataTable
|
|
172
|
+
tableId={tableId}
|
|
173
|
+
data={rows}
|
|
174
|
+
columns={tableColumns}
|
|
175
|
+
columnOrder={columnOrder}
|
|
176
|
+
onColumnOrderChange={onColumnOrderChange}
|
|
177
|
+
loading={loading}
|
|
178
|
+
emptyTitle={emptyTitle}
|
|
179
|
+
emptyDescription={emptyDescription}
|
|
180
|
+
shellClassName={shellClassName}
|
|
181
|
+
scrollContainerClassName={scrollContainerClassName}
|
|
182
|
+
tableClassName={tableClassName}
|
|
183
|
+
showRowNumbers
|
|
184
|
+
rowNumberOffset={rowNumberOffset}
|
|
185
|
+
onRowClick={handleInspectRow}
|
|
186
|
+
getRowAriaLabel={(_, rowIndex) =>
|
|
187
|
+
`Inspect row ${rowNumberOffset + rowIndex + 1}`
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
<CellDetailDrawer
|
|
192
|
+
isOpen={!!drawerPayload}
|
|
193
|
+
onClose={() => setDrawerPayload(null)}
|
|
194
|
+
title={drawerPayload?.title ?? 'Row'}
|
|
195
|
+
value={drawerPayload?.value}
|
|
196
|
+
/>
|
|
197
|
+
</>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import type { Completion } from '@codemirror/autocomplete';
|
|
2
|
+
import type { SQLNamespace } from '@codemirror/lang-sql';
|
|
3
|
+
import { format } from 'sql-formatter';
|
|
4
|
+
import { quoteSqlIdentifier } from '../shared/sql';
|
|
5
|
+
import type {
|
|
6
|
+
SqliteColumnInfo,
|
|
7
|
+
SqliteEntity,
|
|
8
|
+
SqliteSchema,
|
|
9
|
+
} from './sqlite-introspection';
|
|
10
|
+
|
|
11
|
+
export type SqlEditorColumnCacheState = {
|
|
12
|
+
databaseId: string | null;
|
|
13
|
+
entries: Record<string, SqliteColumnInfo[]>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SqlEditorColumnCompletionRequest = {
|
|
17
|
+
schemaName: string | null;
|
|
18
|
+
entityName: string;
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SqlEditorAliasLookup = Record<
|
|
24
|
+
string,
|
|
25
|
+
{
|
|
26
|
+
schemaName: string | null;
|
|
27
|
+
entityName: string;
|
|
28
|
+
}
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
const SQL_IDENTIFIER_PATTERN =
|
|
32
|
+
'"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*';
|
|
33
|
+
|
|
34
|
+
const bareIdentifierPattern = /^[A-Za-z_][\w$]*$/;
|
|
35
|
+
const trailingIdentifierPattern = /[A-Za-z_][\w$]*$/;
|
|
36
|
+
const entityMemberPattern = new RegExp(
|
|
37
|
+
`(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`,
|
|
38
|
+
);
|
|
39
|
+
const singleMemberPattern = new RegExp(
|
|
40
|
+
`(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`,
|
|
41
|
+
);
|
|
42
|
+
const aliasPattern = new RegExp(
|
|
43
|
+
`\\b(?:FROM|JOIN|UPDATE|INTO)\\s+(?:(?:(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*)?(${SQL_IDENTIFIER_PATTERN}))(?:\\s+(?:AS\\s+)?(${SQL_IDENTIFIER_PATTERN}))?`,
|
|
44
|
+
'gi',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const normalizeIdentifier = (identifier: string) => identifier.toLowerCase();
|
|
48
|
+
|
|
49
|
+
const unquoteIdentifier = (identifier: string) => {
|
|
50
|
+
if (identifier.startsWith('"') && identifier.endsWith('"')) {
|
|
51
|
+
return identifier.slice(1, -1).replace(/""/g, '"');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (identifier.startsWith('`') && identifier.endsWith('`')) {
|
|
55
|
+
return identifier.slice(1, -1).replace(/``/g, '`');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (identifier.startsWith('[') && identifier.endsWith(']')) {
|
|
59
|
+
return identifier.slice(1, -1).replace(/]]/g, ']');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return identifier;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getIdentifierInsertText = (identifier: string) =>
|
|
66
|
+
bareIdentifierPattern.test(identifier)
|
|
67
|
+
? identifier
|
|
68
|
+
: quoteSqlIdentifier(identifier);
|
|
69
|
+
|
|
70
|
+
const getColumnDetail = (column: SqliteColumnInfo) => {
|
|
71
|
+
const parts = [column.type].filter(Boolean);
|
|
72
|
+
|
|
73
|
+
if (column.primaryKeyOrder > 0) {
|
|
74
|
+
parts.push('PK');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (column.notNull) {
|
|
78
|
+
parts.push('NOT NULL');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (column.hidden > 0) {
|
|
82
|
+
parts.push('HIDDEN');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join(' · ');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const createSqlEditorColumnCache = (
|
|
89
|
+
databaseId: string | null = null,
|
|
90
|
+
): SqlEditorColumnCacheState => ({
|
|
91
|
+
databaseId,
|
|
92
|
+
entries: {},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const syncSqlEditorColumnCacheDatabase = (
|
|
96
|
+
state: SqlEditorColumnCacheState,
|
|
97
|
+
databaseId: string | null,
|
|
98
|
+
) =>
|
|
99
|
+
state.databaseId === databaseId
|
|
100
|
+
? state
|
|
101
|
+
: createSqlEditorColumnCache(databaseId);
|
|
102
|
+
|
|
103
|
+
export const getSqlEditorColumnCacheKey = (
|
|
104
|
+
databaseId: string,
|
|
105
|
+
schemaName: string,
|
|
106
|
+
entityName: string,
|
|
107
|
+
) => JSON.stringify([databaseId, schemaName, entityName]);
|
|
108
|
+
|
|
109
|
+
export const getSqlEditorCachedColumns = (
|
|
110
|
+
state: SqlEditorColumnCacheState,
|
|
111
|
+
databaseId: string,
|
|
112
|
+
schemaName: string,
|
|
113
|
+
entityName: string,
|
|
114
|
+
) =>
|
|
115
|
+
state.entries[getSqlEditorColumnCacheKey(databaseId, schemaName, entityName)];
|
|
116
|
+
|
|
117
|
+
export const setSqlEditorCachedColumns = (
|
|
118
|
+
state: SqlEditorColumnCacheState,
|
|
119
|
+
databaseId: string,
|
|
120
|
+
schemaName: string,
|
|
121
|
+
entityName: string,
|
|
122
|
+
columns: SqliteColumnInfo[],
|
|
123
|
+
): SqlEditorColumnCacheState => {
|
|
124
|
+
const nextState = syncSqlEditorColumnCacheDatabase(state, databaseId);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
databaseId: nextState.databaseId,
|
|
128
|
+
entries: {
|
|
129
|
+
...nextState.entries,
|
|
130
|
+
[getSqlEditorColumnCacheKey(databaseId, schemaName, entityName)]: columns,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const formatSqlScript = (value: string) => {
|
|
136
|
+
const trimmed = value.trim();
|
|
137
|
+
|
|
138
|
+
if (!trimmed) {
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return format(trimmed, {
|
|
143
|
+
language: 'sqlite',
|
|
144
|
+
keywordCase: 'upper',
|
|
145
|
+
linesBetweenQueries: 1,
|
|
146
|
+
tabWidth: 2,
|
|
147
|
+
}).trim();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const buildSqlCompletionSchema = ({
|
|
151
|
+
databaseId,
|
|
152
|
+
schemas,
|
|
153
|
+
entities,
|
|
154
|
+
columnCache,
|
|
155
|
+
}: {
|
|
156
|
+
databaseId: string | null;
|
|
157
|
+
schemas: SqliteSchema[];
|
|
158
|
+
entities: SqliteEntity[];
|
|
159
|
+
columnCache: SqlEditorColumnCacheState;
|
|
160
|
+
}): SQLNamespace => {
|
|
161
|
+
const groupedEntities = new Map<string, SqliteEntity[]>();
|
|
162
|
+
|
|
163
|
+
for (const entity of entities) {
|
|
164
|
+
const schemaEntities = groupedEntities.get(entity.schemaName) ?? [];
|
|
165
|
+
schemaEntities.push(entity);
|
|
166
|
+
groupedEntities.set(entity.schemaName, schemaEntities);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const namespace: Record<string, SQLNamespace> = {};
|
|
170
|
+
|
|
171
|
+
for (const schema of schemas) {
|
|
172
|
+
const schemaChildren: Record<string, SQLNamespace> = {};
|
|
173
|
+
const schemaEntities = groupedEntities.get(schema.name) ?? [];
|
|
174
|
+
|
|
175
|
+
for (const entity of schemaEntities) {
|
|
176
|
+
const columns =
|
|
177
|
+
databaseId == null
|
|
178
|
+
? []
|
|
179
|
+
: (getSqlEditorCachedColumns(
|
|
180
|
+
columnCache,
|
|
181
|
+
databaseId,
|
|
182
|
+
entity.schemaName,
|
|
183
|
+
entity.name,
|
|
184
|
+
) ?? []);
|
|
185
|
+
|
|
186
|
+
schemaChildren[entity.name] = {
|
|
187
|
+
self: {
|
|
188
|
+
label: entity.name,
|
|
189
|
+
apply: getIdentifierInsertText(entity.name),
|
|
190
|
+
detail: entity.type === 'view' ? 'view' : 'table',
|
|
191
|
+
boost: entity.type === 'view' ? 90 : 100,
|
|
192
|
+
type: entity.type === 'view' ? 'type' : 'class',
|
|
193
|
+
},
|
|
194
|
+
children: columns.map((column) => ({
|
|
195
|
+
label: column.name,
|
|
196
|
+
apply: getIdentifierInsertText(column.name),
|
|
197
|
+
detail: getColumnDetail(column) || undefined,
|
|
198
|
+
boost: column.primaryKeyOrder > 0 ? 80 : 70,
|
|
199
|
+
type: 'property',
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
namespace[schema.name] = {
|
|
205
|
+
self: {
|
|
206
|
+
label: schema.name,
|
|
207
|
+
apply: getIdentifierInsertText(schema.name),
|
|
208
|
+
detail: 'schema',
|
|
209
|
+
boost: 10,
|
|
210
|
+
type: 'namespace',
|
|
211
|
+
},
|
|
212
|
+
children: schemaChildren,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return namespace;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const getDefaultSqlCompletionSchema = (schemas: SqliteSchema[]) =>
|
|
220
|
+
schemas.find((schema) => schema.name === 'main')?.name ?? schemas[0]?.name;
|
|
221
|
+
|
|
222
|
+
export const createSqlColumnCompletions = (columns: SqliteColumnInfo[]) =>
|
|
223
|
+
columns.map(
|
|
224
|
+
(column): Completion => ({
|
|
225
|
+
label: column.name,
|
|
226
|
+
apply: getIdentifierInsertText(column.name),
|
|
227
|
+
detail: getColumnDetail(column) || undefined,
|
|
228
|
+
type: 'property',
|
|
229
|
+
boost: column.primaryKeyOrder > 0 ? 1 : 0,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
export const extractSqlEditorAliases = (
|
|
234
|
+
sqlBeforeCursor: string,
|
|
235
|
+
): SqlEditorAliasLookup => {
|
|
236
|
+
const aliases: SqlEditorAliasLookup = {};
|
|
237
|
+
|
|
238
|
+
let match: RegExpExecArray | null;
|
|
239
|
+
while ((match = aliasPattern.exec(sqlBeforeCursor)) !== null) {
|
|
240
|
+
const schemaName = match[1] ? unquoteIdentifier(match[1]) : null;
|
|
241
|
+
const entityName = unquoteIdentifier(match[2]);
|
|
242
|
+
const alias = match[3] ? unquoteIdentifier(match[3]) : null;
|
|
243
|
+
|
|
244
|
+
if (alias) {
|
|
245
|
+
aliases[normalizeIdentifier(alias)] = { schemaName, entityName };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return aliases;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const getSqlEditorColumnCompletionRequest = (
|
|
253
|
+
sql: string,
|
|
254
|
+
cursorPosition: number,
|
|
255
|
+
): SqlEditorColumnCompletionRequest | null => {
|
|
256
|
+
const beforeCursor = sql.slice(0, cursorPosition);
|
|
257
|
+
const trailingIdentifier = beforeCursor.match(trailingIdentifierPattern)?.[0];
|
|
258
|
+
const replacementLength = trailingIdentifier?.length ?? 0;
|
|
259
|
+
const lookupPrefix = beforeCursor.slice(
|
|
260
|
+
0,
|
|
261
|
+
beforeCursor.length - replacementLength,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const qualifiedMatch = lookupPrefix.match(entityMemberPattern);
|
|
265
|
+
if (qualifiedMatch) {
|
|
266
|
+
return {
|
|
267
|
+
schemaName: unquoteIdentifier(qualifiedMatch[1]),
|
|
268
|
+
entityName: unquoteIdentifier(qualifiedMatch[2]),
|
|
269
|
+
from: cursorPosition - replacementLength,
|
|
270
|
+
to: cursorPosition,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const memberMatch = lookupPrefix.match(singleMemberPattern);
|
|
275
|
+
if (memberMatch) {
|
|
276
|
+
return {
|
|
277
|
+
schemaName: null,
|
|
278
|
+
entityName: unquoteIdentifier(memberMatch[1]),
|
|
279
|
+
from: cursorPosition - replacementLength,
|
|
280
|
+
to: cursorPosition,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const resolveSqlEditorEntityReference = ({
|
|
288
|
+
aliases,
|
|
289
|
+
entities,
|
|
290
|
+
request,
|
|
291
|
+
selectedSchemaName,
|
|
292
|
+
}: {
|
|
293
|
+
aliases: SqlEditorAliasLookup;
|
|
294
|
+
entities: SqliteEntity[];
|
|
295
|
+
request: SqlEditorColumnCompletionRequest;
|
|
296
|
+
selectedSchemaName: string | null;
|
|
297
|
+
}): SqliteEntity | null => {
|
|
298
|
+
const findExactEntity = (schemaName: string, entityName: string) =>
|
|
299
|
+
entities.find(
|
|
300
|
+
(entity) =>
|
|
301
|
+
normalizeIdentifier(entity.schemaName) ===
|
|
302
|
+
normalizeIdentifier(schemaName) &&
|
|
303
|
+
normalizeIdentifier(entity.name) === normalizeIdentifier(entityName),
|
|
304
|
+
) ?? null;
|
|
305
|
+
|
|
306
|
+
if (request.schemaName) {
|
|
307
|
+
return findExactEntity(request.schemaName, request.entityName);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const aliasMatch = aliases[normalizeIdentifier(request.entityName)];
|
|
311
|
+
if (aliasMatch) {
|
|
312
|
+
if (aliasMatch.schemaName) {
|
|
313
|
+
return findExactEntity(aliasMatch.schemaName, aliasMatch.entityName);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
entities.find(
|
|
318
|
+
(entity) =>
|
|
319
|
+
normalizeIdentifier(entity.name) ===
|
|
320
|
+
normalizeIdentifier(aliasMatch.entityName),
|
|
321
|
+
) ?? null
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const entityMatches = entities.filter(
|
|
326
|
+
(entity) =>
|
|
327
|
+
normalizeIdentifier(entity.name) ===
|
|
328
|
+
normalizeIdentifier(request.entityName),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (entityMatches.length === 0) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (selectedSchemaName) {
|
|
336
|
+
const schemaMatch = entityMatches.find(
|
|
337
|
+
(entity) =>
|
|
338
|
+
normalizeIdentifier(entity.schemaName) ===
|
|
339
|
+
normalizeIdentifier(selectedSchemaName),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (schemaMatch) {
|
|
343
|
+
return schemaMatch;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
entityMatches.find(
|
|
349
|
+
(entity) => normalizeIdentifier(entity.schemaName) === 'main',
|
|
350
|
+
) ?? entityMatches[0]
|
|
351
|
+
);
|
|
352
|
+
};
|