@quereus/quereus 0.4.11 → 0.5.1
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/README.md +40 -16
- package/dist/src/core/database.d.ts +18 -0
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +22 -0
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/func/builtins/conversion.d.ts +51 -0
- package/dist/src/func/builtins/conversion.d.ts.map +1 -0
- package/dist/src/func/builtins/conversion.js +152 -0
- package/dist/src/func/builtins/conversion.js.map +1 -0
- package/dist/src/func/builtins/index.d.ts.map +1 -1
- package/dist/src/func/builtins/index.js +20 -1
- package/dist/src/func/builtins/index.js.map +1 -1
- package/dist/src/func/builtins/json.d.ts +1 -0
- package/dist/src/func/builtins/json.d.ts.map +1 -1
- package/dist/src/func/builtins/json.js +100 -0
- package/dist/src/func/builtins/json.js.map +1 -1
- package/dist/src/func/builtins/schema.js +1 -1
- package/dist/src/func/builtins/schema.js.map +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/runtime/emit/binary.d.ts.map +1 -1
- package/dist/src/runtime/emit/binary.js +6 -2
- package/dist/src/runtime/emit/binary.js.map +1 -1
- package/dist/src/runtime/emit/constraint-check.d.ts.map +1 -1
- package/dist/src/runtime/emit/constraint-check.js +8 -1
- package/dist/src/runtime/emit/constraint-check.js.map +1 -1
- package/dist/src/runtime/emit/insert.d.ts.map +1 -1
- package/dist/src/runtime/emit/insert.js +4 -24
- package/dist/src/runtime/emit/insert.js.map +1 -1
- package/dist/src/runtime/emit/scalar-function.d.ts +9 -0
- package/dist/src/runtime/emit/scalar-function.d.ts.map +1 -1
- package/dist/src/runtime/emit/scalar-function.js +23 -1
- package/dist/src/runtime/emit/scalar-function.js.map +1 -1
- package/dist/src/runtime/emit/unary.d.ts.map +1 -1
- package/dist/src/runtime/emit/unary.js +3 -1
- package/dist/src/runtime/emit/unary.js.map +1 -1
- package/dist/src/schema/column.d.ts +4 -1
- package/dist/src/schema/column.d.ts.map +1 -1
- package/dist/src/schema/column.js +3 -0
- package/dist/src/schema/column.js.map +1 -1
- package/dist/src/schema/function.d.ts +15 -0
- package/dist/src/schema/function.d.ts.map +1 -1
- package/dist/src/schema/function.js.map +1 -1
- package/dist/src/schema/table.d.ts.map +1 -1
- package/dist/src/schema/table.js +10 -1
- package/dist/src/schema/table.js.map +1 -1
- package/dist/src/types/builtin-types.d.ts +37 -0
- package/dist/src/types/builtin-types.d.ts.map +1 -0
- package/dist/src/types/builtin-types.js +359 -0
- package/dist/src/types/builtin-types.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +13 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/json-type.d.ts +8 -0
- package/dist/src/types/json-type.d.ts.map +1 -0
- package/dist/src/types/json-type.js +143 -0
- package/dist/src/types/json-type.js.map +1 -0
- package/dist/src/types/logical-type.d.ts +52 -0
- package/dist/src/types/logical-type.d.ts.map +1 -0
- package/dist/src/types/logical-type.js +34 -0
- package/dist/src/types/logical-type.js.map +1 -0
- package/dist/src/types/plugin-interface.d.ts +9 -0
- package/dist/src/types/plugin-interface.d.ts.map +1 -0
- package/dist/src/types/plugin-interface.js +2 -0
- package/dist/src/types/plugin-interface.js.map +1 -0
- package/dist/src/types/registry.d.ts +72 -0
- package/dist/src/types/registry.d.ts.map +1 -0
- package/dist/src/types/registry.js +168 -0
- package/dist/src/types/registry.js.map +1 -0
- package/dist/src/types/temporal-types.d.ts +17 -0
- package/dist/src/types/temporal-types.d.ts.map +1 -0
- package/dist/src/types/temporal-types.js +178 -0
- package/dist/src/types/temporal-types.js.map +1 -0
- package/dist/src/types/validation.d.ts +52 -0
- package/dist/src/types/validation.d.ts.map +1 -0
- package/dist/src/types/validation.js +96 -0
- package/dist/src/types/validation.js.map +1 -0
- package/dist/src/util/comparison.d.ts +24 -5
- package/dist/src/util/comparison.d.ts.map +1 -1
- package/dist/src/util/comparison.js +71 -9
- package/dist/src/util/comparison.js.map +1 -1
- package/dist/src/util/plugin-loader.d.ts.map +1 -1
- package/dist/src/util/plugin-loader.js +17 -0
- package/dist/src/util/plugin-loader.js.map +1 -1
- package/dist/src/vtab/manifest.d.ts +4 -0
- package/dist/src/vtab/manifest.d.ts.map +1 -1
- package/dist/src/vtab/memory/index.d.ts +1 -0
- package/dist/src/vtab/memory/index.d.ts.map +1 -1
- package/dist/src/vtab/memory/index.js +15 -4
- package/dist/src/vtab/memory/index.js.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +20 -2
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
- package/dist/src/vtab/memory/utils/primary-key.js +17 -12
- package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
- package/package.json +5 -4
- package/src/core/database.ts +24 -0
- package/src/func/builtins/conversion.ts +201 -0
- package/src/func/builtins/index.ts +20 -1
- package/src/func/builtins/json.ts +121 -0
- package/src/func/builtins/schema.ts +1 -1
- package/src/index.ts +35 -0
- package/src/runtime/emit/binary.ts +8 -2
- package/src/runtime/emit/constraint-check.ts +9 -1
- package/src/runtime/emit/insert.ts +4 -16
- package/src/runtime/emit/scalar-function.ts +27 -1
- package/src/runtime/emit/unary.ts +4 -1
- package/src/schema/column.ts +8 -1
- package/src/schema/function.ts +18 -0
- package/src/schema/table.ts +411 -398
- package/src/types/builtin-types.ts +350 -0
- package/src/types/index.ts +17 -0
- package/src/types/json-type.ts +152 -0
- package/src/types/logical-type.ts +75 -0
- package/src/types/plugin-interface.ts +10 -0
- package/src/types/registry.ts +200 -0
- package/src/types/temporal-types.ts +167 -0
- package/src/types/validation.ts +120 -0
- package/src/util/comparison.ts +87 -14
- package/src/util/plugin-loader.ts +19 -0
- package/src/vtab/manifest.ts +7 -1
- package/src/vtab/memory/index.ts +191 -178
- package/src/vtab/memory/layer/manager.ts +28 -2
- package/src/vtab/memory/utils/primary-key.ts +19 -14
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { LogicalType } from './logical-type.js';
|
|
2
|
+
import {
|
|
3
|
+
NULL_TYPE,
|
|
4
|
+
INTEGER_TYPE,
|
|
5
|
+
REAL_TYPE,
|
|
6
|
+
TEXT_TYPE,
|
|
7
|
+
BLOB_TYPE,
|
|
8
|
+
BOOLEAN_TYPE,
|
|
9
|
+
NUMERIC_TYPE,
|
|
10
|
+
ANY_TYPE,
|
|
11
|
+
} from './builtin-types.js';
|
|
12
|
+
import { DATE_TYPE, TIME_TYPE, DATETIME_TYPE } from './temporal-types.js';
|
|
13
|
+
import { JSON_TYPE } from './json-type.js';
|
|
14
|
+
import { createLogger } from '../common/logger.js';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('types:registry');
|
|
17
|
+
const warnLog = log.extend('warn');
|
|
18
|
+
const debugLog = log.extend('debug');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Global type registry that maps type names to logical type definitions.
|
|
22
|
+
*/
|
|
23
|
+
class TypeRegistry {
|
|
24
|
+
private types = new Map<string, LogicalType>();
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
// Register built-in types
|
|
28
|
+
this.registerType(NULL_TYPE);
|
|
29
|
+
this.registerType(INTEGER_TYPE);
|
|
30
|
+
this.registerType(REAL_TYPE);
|
|
31
|
+
this.registerType(TEXT_TYPE);
|
|
32
|
+
this.registerType(BLOB_TYPE);
|
|
33
|
+
this.registerType(BOOLEAN_TYPE);
|
|
34
|
+
this.registerType(NUMERIC_TYPE);
|
|
35
|
+
this.registerType(ANY_TYPE);
|
|
36
|
+
this.registerType(DATE_TYPE);
|
|
37
|
+
this.registerType(TIME_TYPE);
|
|
38
|
+
this.registerType(DATETIME_TYPE);
|
|
39
|
+
this.registerType(JSON_TYPE);
|
|
40
|
+
|
|
41
|
+
// Register common aliases
|
|
42
|
+
this.types.set('INT', INTEGER_TYPE);
|
|
43
|
+
this.types.set('BIGINT', INTEGER_TYPE);
|
|
44
|
+
this.types.set('SMALLINT', INTEGER_TYPE);
|
|
45
|
+
this.types.set('TINYINT', INTEGER_TYPE);
|
|
46
|
+
this.types.set('MEDIUMINT', INTEGER_TYPE);
|
|
47
|
+
|
|
48
|
+
this.types.set('FLOAT', REAL_TYPE);
|
|
49
|
+
this.types.set('DOUBLE', REAL_TYPE);
|
|
50
|
+
this.types.set('DECIMAL', NUMERIC_TYPE);
|
|
51
|
+
|
|
52
|
+
this.types.set('VARCHAR', TEXT_TYPE);
|
|
53
|
+
this.types.set('CHAR', TEXT_TYPE);
|
|
54
|
+
this.types.set('CHARACTER', TEXT_TYPE);
|
|
55
|
+
this.types.set('CLOB', TEXT_TYPE);
|
|
56
|
+
this.types.set('STRING', TEXT_TYPE);
|
|
57
|
+
|
|
58
|
+
this.types.set('BOOL', BOOLEAN_TYPE);
|
|
59
|
+
|
|
60
|
+
this.types.set('BYTES', BLOB_TYPE);
|
|
61
|
+
this.types.set('BINARY', BLOB_TYPE);
|
|
62
|
+
this.types.set('VARBINARY', BLOB_TYPE);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a new logical type.
|
|
67
|
+
* @param type The logical type to register
|
|
68
|
+
*/
|
|
69
|
+
registerType(type: LogicalType): void {
|
|
70
|
+
const upperName = type.name.toUpperCase();
|
|
71
|
+
if (this.types.has(upperName)) {
|
|
72
|
+
warnLog(`Overwriting existing type: ${upperName}`);
|
|
73
|
+
}
|
|
74
|
+
this.types.set(upperName, type);
|
|
75
|
+
debugLog(`Registered type: ${upperName}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a logical type by name.
|
|
80
|
+
* @param name The type name (case-insensitive)
|
|
81
|
+
* @returns The logical type, or undefined if not found
|
|
82
|
+
*/
|
|
83
|
+
getType(name: string): LogicalType | undefined {
|
|
84
|
+
return this.types.get(name.toUpperCase());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get a logical type by name, with fallback to BLOB if not found.
|
|
89
|
+
* This matches SQLite's behavior where unknown types default to BLOB affinity.
|
|
90
|
+
* @param name The type name (case-insensitive)
|
|
91
|
+
* @returns The logical type (defaults to BLOB if not found)
|
|
92
|
+
*/
|
|
93
|
+
getTypeOrDefault(name: string | undefined): LogicalType {
|
|
94
|
+
if (!name) return BLOB_TYPE;
|
|
95
|
+
return this.getType(name) ?? BLOB_TYPE;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if a type is registered.
|
|
100
|
+
* @param name The type name (case-insensitive)
|
|
101
|
+
* @returns True if the type is registered
|
|
102
|
+
*/
|
|
103
|
+
hasType(name: string): boolean {
|
|
104
|
+
return this.types.has(name.toUpperCase());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all registered type names.
|
|
109
|
+
* @returns Array of type names
|
|
110
|
+
*/
|
|
111
|
+
getTypeNames(): string[] {
|
|
112
|
+
return Array.from(this.types.keys());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Infer logical type from a type name string.
|
|
117
|
+
* This handles SQLite-style type affinity rules where type names can contain
|
|
118
|
+
* keywords like "INT", "CHAR", "REAL", etc.
|
|
119
|
+
*
|
|
120
|
+
* @param typeName The declared type name (e.g., "VARCHAR(100)", "UNSIGNED INT")
|
|
121
|
+
* @returns The inferred logical type
|
|
122
|
+
*/
|
|
123
|
+
inferType(typeName: string | undefined): LogicalType {
|
|
124
|
+
if (!typeName) return BLOB_TYPE;
|
|
125
|
+
|
|
126
|
+
const upperName = typeName.toUpperCase();
|
|
127
|
+
|
|
128
|
+
// First try exact match
|
|
129
|
+
const exactMatch = this.types.get(upperName);
|
|
130
|
+
if (exactMatch) return exactMatch;
|
|
131
|
+
|
|
132
|
+
// SQLite-style affinity rules
|
|
133
|
+
// INTEGER affinity: INT
|
|
134
|
+
if (upperName.includes('INT')) return INTEGER_TYPE;
|
|
135
|
+
|
|
136
|
+
// TEXT affinity: CHAR, CLOB, TEXT
|
|
137
|
+
if (upperName.includes('CHAR') || upperName.includes('CLOB') || upperName.includes('TEXT')) {
|
|
138
|
+
return TEXT_TYPE;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// BLOB affinity: BLOB
|
|
142
|
+
if (upperName.includes('BLOB')) return BLOB_TYPE;
|
|
143
|
+
|
|
144
|
+
// REAL affinity: REAL, FLOA, DOUB
|
|
145
|
+
if (upperName.includes('REAL') || upperName.includes('FLOA') || upperName.includes('DOUB')) {
|
|
146
|
+
return REAL_TYPE;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// BOOLEAN affinity: BOOL
|
|
150
|
+
if (upperName.includes('BOOL')) return BOOLEAN_TYPE;
|
|
151
|
+
|
|
152
|
+
// NUMERIC affinity: everything else with NUMERIC, DECIMAL
|
|
153
|
+
if (upperName.includes('NUMERIC') || upperName.includes('DECIMAL')) {
|
|
154
|
+
return NUMERIC_TYPE;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Default to BLOB (SQLite behavior)
|
|
158
|
+
debugLog(`Unknown type '${typeName}', defaulting to BLOB`);
|
|
159
|
+
return BLOB_TYPE;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Global singleton instance
|
|
164
|
+
export const typeRegistry = new TypeRegistry();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Register a custom logical type.
|
|
168
|
+
* @param type The logical type to register
|
|
169
|
+
*/
|
|
170
|
+
export function registerType(type: LogicalType): void {
|
|
171
|
+
typeRegistry.registerType(type);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a logical type by name.
|
|
176
|
+
* @param name The type name (case-insensitive)
|
|
177
|
+
* @returns The logical type, or undefined if not found
|
|
178
|
+
*/
|
|
179
|
+
export function getType(name: string): LogicalType | undefined {
|
|
180
|
+
return typeRegistry.getType(name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get a logical type by name, with fallback to BLOB if not found.
|
|
185
|
+
* @param name The type name (case-insensitive)
|
|
186
|
+
* @returns The logical type (defaults to BLOB if not found)
|
|
187
|
+
*/
|
|
188
|
+
export function getTypeOrDefault(name: string | undefined): LogicalType {
|
|
189
|
+
return typeRegistry.getTypeOrDefault(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Infer logical type from a type name string.
|
|
194
|
+
* @param typeName The declared type name
|
|
195
|
+
* @returns The inferred logical type
|
|
196
|
+
*/
|
|
197
|
+
export function inferType(typeName: string | undefined): LogicalType {
|
|
198
|
+
return typeRegistry.inferType(typeName);
|
|
199
|
+
}
|
|
200
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { PhysicalType, type LogicalType } from './logical-type.js';
|
|
2
|
+
import { Temporal } from 'temporal-polyfill';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DATE type - stores ISO 8601 date strings (YYYY-MM-DD)
|
|
6
|
+
* Uses Temporal.PlainDate for validation and parsing
|
|
7
|
+
*/
|
|
8
|
+
export const DATE_TYPE: LogicalType = {
|
|
9
|
+
name: 'DATE',
|
|
10
|
+
physicalType: PhysicalType.TEXT,
|
|
11
|
+
isTemporal: true,
|
|
12
|
+
|
|
13
|
+
validate: (v) => {
|
|
14
|
+
if (v === null) return true;
|
|
15
|
+
if (typeof v !== 'string') return false;
|
|
16
|
+
try {
|
|
17
|
+
Temporal.PlainDate.from(v);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
parse: (v) => {
|
|
25
|
+
if (v === null) return null;
|
|
26
|
+
if (typeof v === 'string') {
|
|
27
|
+
try {
|
|
28
|
+
const date = Temporal.PlainDate.from(v);
|
|
29
|
+
return date.toString(); // ISO 8601 format: YYYY-MM-DD
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new TypeError(`Cannot convert '${v}' to DATE: ${e instanceof Error ? e.message : String(e)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (typeof v === 'number') {
|
|
35
|
+
// Unix timestamp (milliseconds)
|
|
36
|
+
const instant = Temporal.Instant.fromEpochMilliseconds(v);
|
|
37
|
+
return instant.toZonedDateTimeISO('UTC').toPlainDate().toString();
|
|
38
|
+
}
|
|
39
|
+
throw new TypeError(`Cannot convert ${typeof v} to DATE`);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
compare: (a, b) => {
|
|
43
|
+
if (a === null && b === null) return 0;
|
|
44
|
+
if (a === null) return -1;
|
|
45
|
+
if (b === null) return 1;
|
|
46
|
+
// ISO 8601 dates can be compared lexicographically
|
|
47
|
+
return (a as string).localeCompare(b as string);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
supportedCollations: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* TIME type - stores ISO 8601 time strings (HH:MM:SS or HH:MM:SS.sss)
|
|
55
|
+
* Uses Temporal.PlainTime for validation and parsing
|
|
56
|
+
*/
|
|
57
|
+
export const TIME_TYPE: LogicalType = {
|
|
58
|
+
name: 'TIME',
|
|
59
|
+
physicalType: PhysicalType.TEXT,
|
|
60
|
+
isTemporal: true,
|
|
61
|
+
|
|
62
|
+
validate: (v) => {
|
|
63
|
+
if (v === null) return true;
|
|
64
|
+
if (typeof v !== 'string') return false;
|
|
65
|
+
try {
|
|
66
|
+
Temporal.PlainTime.from(v);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
parse: (v) => {
|
|
74
|
+
if (v === null) return null;
|
|
75
|
+
if (typeof v === 'string') {
|
|
76
|
+
try {
|
|
77
|
+
const time = Temporal.PlainTime.from(v);
|
|
78
|
+
return time.toString(); // ISO 8601 format: HH:MM:SS or HH:MM:SS.sss
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new TypeError(`Cannot convert '${v}' to TIME: ${e instanceof Error ? e.message : String(e)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (typeof v === 'number') {
|
|
84
|
+
// Seconds since midnight
|
|
85
|
+
const hours = Math.floor(v / 3600) % 24;
|
|
86
|
+
const minutes = Math.floor((v % 3600) / 60);
|
|
87
|
+
const seconds = v % 60;
|
|
88
|
+
const time = new Temporal.PlainTime(hours, minutes, seconds);
|
|
89
|
+
return time.toString();
|
|
90
|
+
}
|
|
91
|
+
throw new TypeError(`Cannot convert ${typeof v} to TIME`);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
compare: (a, b) => {
|
|
95
|
+
if (a === null && b === null) return 0;
|
|
96
|
+
if (a === null) return -1;
|
|
97
|
+
if (b === null) return 1;
|
|
98
|
+
// ISO 8601 times can be compared lexicographically
|
|
99
|
+
return (a as string).localeCompare(b as string);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
supportedCollations: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* DATETIME type - stores ISO 8601 datetime strings (YYYY-MM-DDTHH:MM:SS or with timezone)
|
|
107
|
+
* Uses Temporal.PlainDateTime for validation and parsing
|
|
108
|
+
*/
|
|
109
|
+
export const DATETIME_TYPE: LogicalType = {
|
|
110
|
+
name: 'DATETIME',
|
|
111
|
+
physicalType: PhysicalType.TEXT,
|
|
112
|
+
isTemporal: true,
|
|
113
|
+
|
|
114
|
+
validate: (v) => {
|
|
115
|
+
if (v === null) return true;
|
|
116
|
+
if (typeof v !== 'string') return false;
|
|
117
|
+
try {
|
|
118
|
+
// Try PlainDateTime first
|
|
119
|
+
Temporal.PlainDateTime.from(v);
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
try {
|
|
123
|
+
// Also accept ZonedDateTime
|
|
124
|
+
Temporal.ZonedDateTime.from(v);
|
|
125
|
+
return true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
parse: (v) => {
|
|
133
|
+
if (v === null) return null;
|
|
134
|
+
if (typeof v === 'string') {
|
|
135
|
+
try {
|
|
136
|
+
// Try PlainDateTime first
|
|
137
|
+
const dt = Temporal.PlainDateTime.from(v);
|
|
138
|
+
return dt.toString(); // ISO 8601 format: YYYY-MM-DDTHH:MM:SS
|
|
139
|
+
} catch {
|
|
140
|
+
try {
|
|
141
|
+
// Try ZonedDateTime
|
|
142
|
+
const zdt = Temporal.ZonedDateTime.from(v);
|
|
143
|
+
return zdt.toString(); // ISO 8601 with timezone
|
|
144
|
+
} catch (e) {
|
|
145
|
+
throw new TypeError(`Cannot convert '${v}' to DATETIME: ${e instanceof Error ? e.message : String(e)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (typeof v === 'number') {
|
|
150
|
+
// Unix timestamp (milliseconds)
|
|
151
|
+
const instant = Temporal.Instant.fromEpochMilliseconds(v);
|
|
152
|
+
return instant.toZonedDateTimeISO('UTC').toString();
|
|
153
|
+
}
|
|
154
|
+
throw new TypeError(`Cannot convert ${typeof v} to DATETIME`);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
compare: (a, b) => {
|
|
158
|
+
if (a === null && b === null) return 0;
|
|
159
|
+
if (a === null) return -1;
|
|
160
|
+
if (b === null) return 1;
|
|
161
|
+
// ISO 8601 datetimes can be compared lexicographically
|
|
162
|
+
return (a as string).localeCompare(b as string);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
supportedCollations: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { SqlValue } from '../common/types.js';
|
|
2
|
+
import { StatusCode } from '../common/types.js';
|
|
3
|
+
import { QuereusError } from '../common/errors.js';
|
|
4
|
+
import type { LogicalType } from './logical-type.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate a value against a logical type.
|
|
8
|
+
* Throws an error if the value is invalid.
|
|
9
|
+
*
|
|
10
|
+
* @param value The value to validate
|
|
11
|
+
* @param type The logical type to validate against
|
|
12
|
+
* @param columnName Optional column name for better error messages
|
|
13
|
+
* @returns The validated value
|
|
14
|
+
* @throws QuereusError if validation fails
|
|
15
|
+
*/
|
|
16
|
+
export function validateValue(
|
|
17
|
+
value: SqlValue,
|
|
18
|
+
type: LogicalType,
|
|
19
|
+
columnName?: string
|
|
20
|
+
): SqlValue {
|
|
21
|
+
// NULL is always valid
|
|
22
|
+
if (value === null) return null;
|
|
23
|
+
|
|
24
|
+
// Type-specific validation
|
|
25
|
+
if (type.validate && !type.validate(value)) {
|
|
26
|
+
const colInfo = columnName ? ` for column '${columnName}'` : '';
|
|
27
|
+
throw new QuereusError(
|
|
28
|
+
`Type mismatch${colInfo}: expected ${type.name}, got ${typeof value}`,
|
|
29
|
+
StatusCode.MISMATCH
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse/convert a value to match a logical type.
|
|
38
|
+
* This performs type conversion and normalization.
|
|
39
|
+
*
|
|
40
|
+
* @param value The value to parse
|
|
41
|
+
* @param type The logical type to convert to
|
|
42
|
+
* @param columnName Optional column name for better error messages
|
|
43
|
+
* @returns The parsed/converted value
|
|
44
|
+
* @throws QuereusError if conversion fails
|
|
45
|
+
*/
|
|
46
|
+
export function parseValue(
|
|
47
|
+
value: SqlValue,
|
|
48
|
+
type: LogicalType,
|
|
49
|
+
columnName?: string
|
|
50
|
+
): SqlValue {
|
|
51
|
+
// NULL is always valid
|
|
52
|
+
if (value === null) return null;
|
|
53
|
+
|
|
54
|
+
// Type-specific parsing
|
|
55
|
+
if (type.parse) {
|
|
56
|
+
try {
|
|
57
|
+
return type.parse(value);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const colInfo = columnName ? ` for column '${columnName}'` : '';
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
throw new QuereusError(
|
|
62
|
+
`Type conversion failed${colInfo}: ${message}`,
|
|
63
|
+
StatusCode.MISMATCH
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate and parse a value in one step.
|
|
73
|
+
* This is the main entry point for type checking at INSERT/UPDATE boundaries.
|
|
74
|
+
*
|
|
75
|
+
* @param value The value to validate and parse
|
|
76
|
+
* @param type The logical type
|
|
77
|
+
* @param columnName Optional column name for better error messages
|
|
78
|
+
* @returns The validated and parsed value
|
|
79
|
+
* @throws QuereusError if validation or parsing fails
|
|
80
|
+
*/
|
|
81
|
+
export function validateAndParse(
|
|
82
|
+
value: SqlValue,
|
|
83
|
+
type: LogicalType,
|
|
84
|
+
columnName?: string
|
|
85
|
+
): SqlValue {
|
|
86
|
+
// Parse first (which may convert the value)
|
|
87
|
+
const parsed = parseValue(value, type, columnName);
|
|
88
|
+
|
|
89
|
+
// Then validate the parsed result
|
|
90
|
+
return validateValue(parsed, type, columnName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a value is compatible with a logical type without throwing.
|
|
95
|
+
*
|
|
96
|
+
* @param value The value to check
|
|
97
|
+
* @param type The logical type
|
|
98
|
+
* @returns True if the value is valid for the type
|
|
99
|
+
*/
|
|
100
|
+
export function isValidForType(value: SqlValue, type: LogicalType): boolean {
|
|
101
|
+
if (value === null) return true;
|
|
102
|
+
if (!type.validate) return true;
|
|
103
|
+
return type.validate(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Try to parse a value, returning null if parsing fails.
|
|
108
|
+
*
|
|
109
|
+
* @param value The value to parse
|
|
110
|
+
* @param type The logical type
|
|
111
|
+
* @returns The parsed value, or null if parsing fails
|
|
112
|
+
*/
|
|
113
|
+
export function tryParse(value: SqlValue, type: LogicalType): SqlValue | null {
|
|
114
|
+
try {
|
|
115
|
+
return parseValue(value, type);
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
package/src/util/comparison.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Row, SqlValue } from '../common/types.js';
|
|
2
2
|
import { createLogger } from '../common/logger.js';
|
|
3
|
+
import type { LogicalType, CollationFunction } from '../types/logical-type.js';
|
|
4
|
+
import { StatusCode } from '../common/types.js';
|
|
5
|
+
import { QuereusError } from '../common/errors.js';
|
|
3
6
|
|
|
4
7
|
const log = createLogger('util:comparison');
|
|
5
8
|
const warnLog = log.extend('warn');
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Takes two strings and returns a comparison result (-1, 0, 1)
|
|
10
|
-
*/
|
|
11
|
-
export type CollationFunction = (a: string, b: string) => number;
|
|
10
|
+
// Re-export CollationFunction for backward compatibility
|
|
11
|
+
export type { CollationFunction };
|
|
12
12
|
|
|
13
13
|
// Map to store registered collations
|
|
14
14
|
const collations = new Map<string, CollationFunction>();
|
|
@@ -350,9 +350,11 @@ export function compareWithOrderBy(
|
|
|
350
350
|
): number {
|
|
351
351
|
// Convert to optimized flags and use fast path
|
|
352
352
|
const directionFlag = direction === 'desc' ? SortDirection.DESC : SortDirection.ASC;
|
|
353
|
-
const nullsFlag = nullsOrdering === 'first'
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
const nullsFlag = nullsOrdering === 'first'
|
|
354
|
+
? NullsOrdering.FIRST
|
|
355
|
+
: nullsOrdering === 'last'
|
|
356
|
+
? NullsOrdering.LAST
|
|
357
|
+
: NullsOrdering.DEFAULT;
|
|
356
358
|
const collationFunc = collationName === 'BINARY' ? BINARY_COLLATION : resolveCollation(collationName);
|
|
357
359
|
|
|
358
360
|
return compareWithOrderByFast(a, b, directionFlag, nullsFlag, collationFunc);
|
|
@@ -391,9 +393,11 @@ export function createOrderByComparatorFast(
|
|
|
391
393
|
collationFunc: CollationFunction = BINARY_COLLATION
|
|
392
394
|
): (a: SqlValue, b: SqlValue) => number {
|
|
393
395
|
const directionFlag = direction === 'desc' ? SortDirection.DESC : SortDirection.ASC;
|
|
394
|
-
const nullsFlag = nullsOrdering === 'first'
|
|
395
|
-
|
|
396
|
-
|
|
396
|
+
const nullsFlag = nullsOrdering === 'first'
|
|
397
|
+
? NullsOrdering.FIRST
|
|
398
|
+
: nullsOrdering === 'last'
|
|
399
|
+
? NullsOrdering.LAST
|
|
400
|
+
: NullsOrdering.DEFAULT;
|
|
397
401
|
|
|
398
402
|
// Return a closure that captures the pre-resolved values
|
|
399
403
|
return (a: SqlValue, b: SqlValue): number => {
|
|
@@ -432,6 +436,75 @@ export function compareRows(a: Row, b: Row): number {
|
|
|
432
436
|
return 0;
|
|
433
437
|
}
|
|
434
438
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Type-aware comparison function that uses logical type information.
|
|
442
|
+
* This eliminates runtime type detection and uses type-specific comparison logic.
|
|
443
|
+
*
|
|
444
|
+
* @param a First value
|
|
445
|
+
* @param b Second value
|
|
446
|
+
* @param typeA Logical type of first value
|
|
447
|
+
* @param typeB Logical type of second value (should match typeA for strict typing)
|
|
448
|
+
* @param collation Optional collation function for TEXT types
|
|
449
|
+
* @returns -1 if a < b, 0 if a === b, 1 if a > b
|
|
450
|
+
* @throws QuereusError if types don't match (strict typing)
|
|
451
|
+
*/
|
|
452
|
+
export function compareTypedValues(
|
|
453
|
+
a: SqlValue,
|
|
454
|
+
b: SqlValue,
|
|
455
|
+
typeA: LogicalType,
|
|
456
|
+
typeB: LogicalType,
|
|
457
|
+
collation?: CollationFunction
|
|
458
|
+
): number {
|
|
459
|
+
// NULL handling
|
|
460
|
+
if (a === null && b === null) return 0;
|
|
461
|
+
if (a === null) return -1;
|
|
462
|
+
if (b === null) return 1;
|
|
463
|
+
|
|
464
|
+
// Type mismatch error (strict typing)
|
|
465
|
+
if (typeA !== typeB) {
|
|
466
|
+
throw new QuereusError(
|
|
467
|
+
`Type mismatch in comparison: ${typeA.name} vs ${typeB.name}`,
|
|
468
|
+
StatusCode.MISMATCH
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Use type-specific comparison if available
|
|
473
|
+
if (typeA.compare) {
|
|
474
|
+
return typeA.compare(a, b, collation);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Fallback to default comparison based on physical type
|
|
478
|
+
// This shouldn't happen for built-in types, but provides safety for custom types
|
|
479
|
+
return compareSqlValuesFast(a, b, collation ?? BINARY_COLLATION);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Create a type-aware comparator function for a specific logical type.
|
|
484
|
+
* This is optimized for use in indexes and sorts where the type is known at creation time.
|
|
485
|
+
*
|
|
486
|
+
* @param type The logical type
|
|
487
|
+
* @param collation Optional collation function for TEXT types
|
|
488
|
+
* @returns A comparator function
|
|
489
|
+
*/
|
|
490
|
+
export function createTypedComparator(
|
|
491
|
+
type: LogicalType,
|
|
492
|
+
collation?: CollationFunction
|
|
493
|
+
): (a: SqlValue, b: SqlValue) => number {
|
|
494
|
+
// Pre-resolve the comparison function
|
|
495
|
+
const compareFunc = type.compare;
|
|
496
|
+
|
|
497
|
+
if (compareFunc) {
|
|
498
|
+
// Type has custom comparison
|
|
499
|
+
return (a: SqlValue, b: SqlValue) => {
|
|
500
|
+
if (a === null && b === null) return 0;
|
|
501
|
+
if (a === null) return -1;
|
|
502
|
+
if (b === null) return 1;
|
|
503
|
+
return compareFunc(a, b, collation);
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Fallback to default comparison
|
|
508
|
+
const collationFunc = collation ?? BINARY_COLLATION;
|
|
509
|
+
return (a: SqlValue, b: SqlValue) => compareSqlValuesFast(a, b, collationFunc);
|
|
510
|
+
}
|
|
@@ -80,6 +80,10 @@ export async function dynamicLoadModule(
|
|
|
80
80
|
log(' Registered %d collation(s): %s', registrations.collations.length,
|
|
81
81
|
registrations.collations.map(c => c.name).join(', '));
|
|
82
82
|
}
|
|
83
|
+
if (registrations.types?.length) {
|
|
84
|
+
log(' Registered %d type(s): %s', registrations.types.length,
|
|
85
|
+
registrations.types.map(t => t.name).join(', '));
|
|
86
|
+
}
|
|
83
87
|
|
|
84
88
|
// Try to extract manifest from package.json
|
|
85
89
|
let manifest: PluginManifest | undefined;
|
|
@@ -141,6 +145,17 @@ async function registerPluginItems(db: Database, registrations: PluginRegistrati
|
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
}
|
|
148
|
+
|
|
149
|
+
// Register types
|
|
150
|
+
if (registrations.types) {
|
|
151
|
+
for (const type of registrations.types) {
|
|
152
|
+
try {
|
|
153
|
+
db.registerType(type.name, type.definition);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
quereusError(`Failed to register type '${type.name}': ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
/**
|
|
@@ -255,6 +270,10 @@ export async function loadPlugin(
|
|
|
255
270
|
log(' Registered %d collation(s): %s', registrations.collations.length,
|
|
256
271
|
registrations.collations.map(c => c.name).join(', '));
|
|
257
272
|
}
|
|
273
|
+
if (registrations.types?.length) {
|
|
274
|
+
log(' Registered %d type(s): %s', registrations.types.length,
|
|
275
|
+
registrations.types.map(t => t.name).join(', '));
|
|
276
|
+
}
|
|
258
277
|
|
|
259
278
|
// Try to extract manifest from package.json
|
|
260
279
|
let manifest: PluginManifest | undefined;
|
package/src/vtab/manifest.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import type { SqlValue } from '../common/types.js';
|
|
3
3
|
import type { FunctionSchema } from '../schema/function.js';
|
|
4
4
|
import type { CollationFunction } from '../util/comparison.js';
|
|
5
|
+
import type { TypePluginInfo } from '../types/plugin-interface.js';
|
|
6
|
+
|
|
7
|
+
// Re-export TypePluginInfo so it can be imported from this module
|
|
8
|
+
export type { TypePluginInfo };
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Configuration setting definition for a plugin
|
|
@@ -46,6 +50,7 @@ export interface PluginRegistrations {
|
|
|
46
50
|
vtables?: VTablePluginInfo[];
|
|
47
51
|
functions?: FunctionPluginInfo[];
|
|
48
52
|
collations?: CollationPluginInfo[];
|
|
53
|
+
types?: TypePluginInfo[];
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
@@ -59,12 +64,13 @@ export interface PluginManifest {
|
|
|
59
64
|
pragmaPrefix?: string; // default = name, used for PRAGMA commands
|
|
60
65
|
settings?: PluginSetting[]; // configuration options
|
|
61
66
|
capabilities?: string[]; // e.g. ['scan', 'index', 'write']
|
|
62
|
-
|
|
67
|
+
|
|
63
68
|
// Plugin type indicators (for UI display)
|
|
64
69
|
provides?: {
|
|
65
70
|
vtables?: string[]; // names of vtable modules provided
|
|
66
71
|
functions?: string[]; // names of functions provided
|
|
67
72
|
collations?: string[]; // names of collations provided
|
|
73
|
+
types?: string[]; // names of types provided
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
|