@quereus/quereus 0.4.11 → 0.5.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.
Files changed (116) hide show
  1. package/README.md +40 -16
  2. package/dist/src/core/database.d.ts +18 -0
  3. package/dist/src/core/database.d.ts.map +1 -1
  4. package/dist/src/core/database.js +22 -0
  5. package/dist/src/core/database.js.map +1 -1
  6. package/dist/src/func/builtins/conversion.d.ts +51 -0
  7. package/dist/src/func/builtins/conversion.d.ts.map +1 -0
  8. package/dist/src/func/builtins/conversion.js +152 -0
  9. package/dist/src/func/builtins/conversion.js.map +1 -0
  10. package/dist/src/func/builtins/index.d.ts.map +1 -1
  11. package/dist/src/func/builtins/index.js +20 -1
  12. package/dist/src/func/builtins/index.js.map +1 -1
  13. package/dist/src/func/builtins/json.d.ts +1 -0
  14. package/dist/src/func/builtins/json.d.ts.map +1 -1
  15. package/dist/src/func/builtins/json.js +100 -0
  16. package/dist/src/func/builtins/json.js.map +1 -1
  17. package/dist/src/func/builtins/schema.js +1 -1
  18. package/dist/src/func/builtins/schema.js.map +1 -1
  19. package/dist/src/index.d.ts +8 -1
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js +6 -0
  22. package/dist/src/index.js.map +1 -1
  23. package/dist/src/runtime/emit/insert.d.ts.map +1 -1
  24. package/dist/src/runtime/emit/insert.js +4 -24
  25. package/dist/src/runtime/emit/insert.js.map +1 -1
  26. package/dist/src/runtime/emit/scalar-function.d.ts +9 -0
  27. package/dist/src/runtime/emit/scalar-function.d.ts.map +1 -1
  28. package/dist/src/runtime/emit/scalar-function.js +23 -1
  29. package/dist/src/runtime/emit/scalar-function.js.map +1 -1
  30. package/dist/src/schema/column.d.ts +4 -1
  31. package/dist/src/schema/column.d.ts.map +1 -1
  32. package/dist/src/schema/column.js +3 -0
  33. package/dist/src/schema/column.js.map +1 -1
  34. package/dist/src/schema/function.d.ts +15 -0
  35. package/dist/src/schema/function.d.ts.map +1 -1
  36. package/dist/src/schema/function.js.map +1 -1
  37. package/dist/src/schema/table.d.ts.map +1 -1
  38. package/dist/src/schema/table.js +10 -1
  39. package/dist/src/schema/table.js.map +1 -1
  40. package/dist/src/types/builtin-types.d.ts +37 -0
  41. package/dist/src/types/builtin-types.d.ts.map +1 -0
  42. package/dist/src/types/builtin-types.js +345 -0
  43. package/dist/src/types/builtin-types.js.map +1 -0
  44. package/dist/src/types/index.d.ts +7 -0
  45. package/dist/src/types/index.d.ts.map +1 -0
  46. package/dist/src/types/index.js +13 -0
  47. package/dist/src/types/index.js.map +1 -0
  48. package/dist/src/types/json-type.d.ts +8 -0
  49. package/dist/src/types/json-type.d.ts.map +1 -0
  50. package/dist/src/types/json-type.js +143 -0
  51. package/dist/src/types/json-type.js.map +1 -0
  52. package/dist/src/types/logical-type.d.ts +52 -0
  53. package/dist/src/types/logical-type.d.ts.map +1 -0
  54. package/dist/src/types/logical-type.js +34 -0
  55. package/dist/src/types/logical-type.js.map +1 -0
  56. package/dist/src/types/plugin-interface.d.ts +9 -0
  57. package/dist/src/types/plugin-interface.d.ts.map +1 -0
  58. package/dist/src/types/plugin-interface.js +2 -0
  59. package/dist/src/types/plugin-interface.js.map +1 -0
  60. package/dist/src/types/registry.d.ts +72 -0
  61. package/dist/src/types/registry.d.ts.map +1 -0
  62. package/dist/src/types/registry.js +168 -0
  63. package/dist/src/types/registry.js.map +1 -0
  64. package/dist/src/types/temporal-types.d.ts +17 -0
  65. package/dist/src/types/temporal-types.d.ts.map +1 -0
  66. package/dist/src/types/temporal-types.js +178 -0
  67. package/dist/src/types/temporal-types.js.map +1 -0
  68. package/dist/src/types/validation.d.ts +52 -0
  69. package/dist/src/types/validation.d.ts.map +1 -0
  70. package/dist/src/types/validation.js +96 -0
  71. package/dist/src/types/validation.js.map +1 -0
  72. package/dist/src/util/comparison.d.ts +24 -5
  73. package/dist/src/util/comparison.d.ts.map +1 -1
  74. package/dist/src/util/comparison.js +71 -9
  75. package/dist/src/util/comparison.js.map +1 -1
  76. package/dist/src/util/plugin-loader.d.ts.map +1 -1
  77. package/dist/src/util/plugin-loader.js +17 -0
  78. package/dist/src/util/plugin-loader.js.map +1 -1
  79. package/dist/src/vtab/manifest.d.ts +4 -0
  80. package/dist/src/vtab/manifest.d.ts.map +1 -1
  81. package/dist/src/vtab/memory/index.d.ts +1 -0
  82. package/dist/src/vtab/memory/index.d.ts.map +1 -1
  83. package/dist/src/vtab/memory/index.js +15 -4
  84. package/dist/src/vtab/memory/index.js.map +1 -1
  85. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  86. package/dist/src/vtab/memory/layer/manager.js +20 -2
  87. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  88. package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
  89. package/dist/src/vtab/memory/utils/primary-key.js +17 -12
  90. package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
  91. package/package.json +5 -4
  92. package/src/core/database.ts +24 -0
  93. package/src/func/builtins/conversion.ts +201 -0
  94. package/src/func/builtins/index.ts +20 -1
  95. package/src/func/builtins/json.ts +121 -0
  96. package/src/func/builtins/schema.ts +1 -1
  97. package/src/index.ts +35 -0
  98. package/src/runtime/emit/insert.ts +4 -16
  99. package/src/runtime/emit/scalar-function.ts +27 -1
  100. package/src/schema/column.ts +8 -1
  101. package/src/schema/function.ts +18 -0
  102. package/src/schema/table.ts +411 -398
  103. package/src/types/builtin-types.ts +336 -0
  104. package/src/types/index.ts +17 -0
  105. package/src/types/json-type.ts +152 -0
  106. package/src/types/logical-type.ts +75 -0
  107. package/src/types/plugin-interface.ts +10 -0
  108. package/src/types/registry.ts +200 -0
  109. package/src/types/temporal-types.ts +167 -0
  110. package/src/types/validation.ts +120 -0
  111. package/src/util/comparison.ts +87 -14
  112. package/src/util/plugin-loader.ts +19 -0
  113. package/src/vtab/manifest.ts +7 -1
  114. package/src/vtab/memory/index.ts +191 -178
  115. package/src/vtab/memory/layer/manager.ts +28 -2
  116. 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
+
@@ -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
- * Function type for SQLite collation functions.
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' ? NullsOrdering.FIRST :
354
- nullsOrdering === 'last' ? NullsOrdering.LAST :
355
- NullsOrdering.DEFAULT;
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' ? NullsOrdering.FIRST :
395
- nullsOrdering === 'last' ? NullsOrdering.LAST :
396
- NullsOrdering.DEFAULT;
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
- // TODO: The main remaining task for comparison is implementing SQLite's
436
- // type affinity rules (which affect how values are treated BEFORE comparison)
437
- // and handling different TEXT collations.
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;
@@ -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