@aetherframework/database 1.1.0 → 1.1.2

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 (47) hide show
  1. package/examples/mysql-test-pressure.js +1530 -0
  2. package/examples/test-direct.js +116 -0
  3. package/examples/transaction_example.js +127 -0
  4. package/package.json +3 -1
  5. package/src/DatabaseManager.js +565 -0
  6. package/src/core/ConnectionManager.js +351 -0
  7. package/src/core/DatabaseFactory.js +188 -0
  8. package/src/core/MongoQueryBuilder.js +576 -0
  9. package/src/core/PluginManager.js +968 -0
  10. package/src/core/QueryBuilder.js +4394 -0
  11. package/src/core/TransactionManager.js +40 -0
  12. package/src/drivers/clickhouse-driver.js +272 -0
  13. package/src/drivers/index.js +273 -0
  14. package/src/drivers/mongodb-driver.js +87 -0
  15. package/src/drivers/mssql-driver.js +117 -0
  16. package/src/drivers/mysql-driver.js +169 -0
  17. package/src/drivers/oracle-driver.js +101 -0
  18. package/src/drivers/postgres-driver.js +234 -0
  19. package/src/drivers/redis-driver.js +52 -0
  20. package/src/drivers/sqlite-driver.js +67 -0
  21. package/src/middleware/connection-pool.js +455 -0
  22. package/src/middleware/performance-monitor.js +652 -0
  23. package/src/middleware/query-cache.js +500 -0
  24. package/src/middleware/query-logger.js +262 -0
  25. package/src/plugins/AuditPlugin.js +447 -0
  26. package/src/plugins/BasePlugin.js +418 -0
  27. package/src/plugins/BatchOperationPlugin.js +165 -0
  28. package/src/plugins/CachePlugin.js +407 -0
  29. package/src/plugins/CtePlugin.js +523 -0
  30. package/src/plugins/DistributedPlugin.js +543 -0
  31. package/src/plugins/EncryptionPlugin.js +211 -0
  32. package/src/plugins/FullTextSearchPlugin.js +164 -0
  33. package/src/plugins/GeospatialPlugin.js +219 -0
  34. package/src/plugins/GraphQLPlugin.js +162 -0
  35. package/src/plugins/HookPlugin.js +211 -0
  36. package/src/plugins/JsonPlugin.js +366 -0
  37. package/src/plugins/OptimisticLockPlugin.js +374 -0
  38. package/src/plugins/PerformancePlugin.js +175 -0
  39. package/src/plugins/ResiliencePlugin.js +114 -0
  40. package/src/plugins/ShardingPlugin.js +227 -0
  41. package/src/plugins/SoftDeletePlugin.js +258 -0
  42. package/src/plugins/SyncPlugin.js +373 -0
  43. package/src/plugins/VersioningPlugin.js +314 -0
  44. package/src/plugins/WindowFunctionPlugin.js +343 -0
  45. package/src/utils/config-loader.js +632 -0
  46. package/src/utils/error-handler.js +724 -0
  47. package/src/utils/migration-runner.js +1066 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/EncryptionPlugin
6
+ */
7
+ import crypto from 'crypto';
8
+ import { BasePlugin } from './BasePlugin.js';
9
+
10
+ /**
11
+ * Encryption Plugin - Provides data encryption/decryption functionality
12
+ * Supports AES-256-GCM encryption for sensitive fields
13
+ */
14
+ export class EncryptionPlugin extends BasePlugin {
15
+ constructor(queryBuilder) {
16
+ super(queryBuilder);
17
+ this.encryptionKey = null;
18
+ this.encryptedFields = new Set();
19
+ this.encryptionAlgorithm = 'aes-256-gcm';
20
+ }
21
+
22
+ _registerMethods() {
23
+ // Register encryption methods to QueryBuilder
24
+ this.queryBuilder.setEncryptionKey = this.setEncryptionKey.bind(this);
25
+ this.queryBuilder.encryptField = this.encryptField.bind(this);
26
+ this.queryBuilder.encryptData = this.encryptData.bind(this);
27
+ this.queryBuilder.decryptData = this.decryptData.bind(this);
28
+ this.queryBuilder.isEncryptionEnabled = this.isEncryptionEnabled.bind(this);
29
+ }
30
+
31
+ /**
32
+ * Set encryption key
33
+ * @param {string|Buffer} key - Encryption key (32 bytes for AES-256)
34
+ * @returns {QueryBuilder} Query builder instance
35
+ */
36
+ setEncryptionKey(key) {
37
+ if (typeof key === 'string') {
38
+ // Ensure key is 32 bytes for AES-256
39
+ const keyBuffer = Buffer.from(key, 'utf8');
40
+ if (keyBuffer.length !== 32) {
41
+ throw new Error('Encryption key must be 32 bytes for AES-256');
42
+ }
43
+ this.encryptionKey = keyBuffer;
44
+ } else if (Buffer.isBuffer(key)) {
45
+ if (key.length !== 32) {
46
+ throw new Error('Encryption key must be 32 bytes for AES-256');
47
+ }
48
+ this.encryptionKey = key;
49
+ } else {
50
+ throw new Error('Encryption key must be a string or Buffer');
51
+ }
52
+
53
+ // Add encryption hooks
54
+ this._addEncryptionHooks();
55
+ return this.queryBuilder;
56
+ }
57
+
58
+ /**
59
+ * Mark field for encryption
60
+ * @param {string} fieldName - Field name to encrypt
61
+ * @returns {QueryBuilder} Query builder instance
62
+ */
63
+ encryptField(fieldName) {
64
+ this.encryptedFields.add(fieldName);
65
+ return this.queryBuilder;
66
+ }
67
+
68
+ /**
69
+ * Encrypt data before insertion/update
70
+ * @param {Object} data - Data to encrypt
71
+ * @returns {Object} Encrypted data
72
+ */
73
+ encryptData(data) {
74
+ if (!this.encryptionKey || this.encryptedFields.size === 0) {
75
+ return data;
76
+ }
77
+
78
+ const encrypted = { ...data };
79
+
80
+ for (const field of this.encryptedFields) {
81
+ if (encrypted[field] !== undefined && encrypted[field] !== null) {
82
+ try {
83
+ const iv = crypto.randomBytes(16);
84
+ const cipher = crypto.createCipheriv(
85
+ this.encryptionAlgorithm,
86
+ this.encryptionKey,
87
+ iv
88
+ );
89
+
90
+ let encryptedText = cipher.update(
91
+ String(encrypted[field]),
92
+ 'utf8',
93
+ 'hex'
94
+ );
95
+ encryptedText += cipher.final('hex');
96
+ const authTag = cipher.getAuthTag();
97
+
98
+ // Store format: iv:authTag:encryptedText
99
+ encrypted[field] =
100
+ `${iv.toString('hex')}:${authTag.toString('hex')}:${encryptedText}`;
101
+ } catch (error) {
102
+ console.error(`Failed to encrypt field ${field}:`, error.message);
103
+ throw new Error(`Encryption failed for field ${field}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ return encrypted;
109
+ }
110
+
111
+ /**
112
+ * Decrypt data after retrieval
113
+ * @param {Object} data - Data to decrypt
114
+ * @returns {Object} Decrypted data
115
+ */
116
+ decryptData(data) {
117
+ if (!this.encryptionKey || this.encryptedFields.size === 0) {
118
+ return data;
119
+ }
120
+
121
+ const decrypted = { ...data };
122
+
123
+ for (const field of this.encryptedFields) {
124
+ if (decrypted[field] && decrypted[field].includes(':')) {
125
+ try {
126
+ const [ivHex, authTagHex, encryptedText] = decrypted[field].split(':');
127
+ const iv = Buffer.from(ivHex, 'hex');
128
+ const authTag = Buffer.from(authTagHex, 'hex');
129
+
130
+ const decipher = crypto.createDecipheriv(
131
+ this.encryptionAlgorithm,
132
+ this.encryptionKey,
133
+ iv
134
+ );
135
+ decipher.setAuthTag(authTag);
136
+
137
+ let decryptedText = decipher.update(encryptedText, 'hex', 'utf8');
138
+ decryptedText += decipher.final('utf8');
139
+
140
+ decrypted[field] = decryptedText;
141
+ } catch (error) {
142
+ console.warn(`Failed to decrypt field ${field}:`, error.message);
143
+ decrypted[field] = null;
144
+ }
145
+ }
146
+ }
147
+
148
+ return decrypted;
149
+ }
150
+
151
+ /**
152
+ * Check if encryption is enabled
153
+ * @returns {boolean} True if encryption is enabled
154
+ */
155
+ isEncryptionEnabled() {
156
+ return this.encryptionKey !== null && this.encryptedFields.size > 0;
157
+ }
158
+
159
+ /**
160
+ * Add encryption hooks to QueryBuilder
161
+ * @private
162
+ */
163
+ _addEncryptionHooks() {
164
+ // Hook for encrypting data before insert/update
165
+ this.queryBuilder.addHook('beforeInsert', async (data) => {
166
+ return this.encryptData(data);
167
+ });
168
+
169
+ this.queryBuilder.addHook('beforeUpdate', async (data) => {
170
+ return this.encryptData(data);
171
+ });
172
+
173
+ // Hook for decrypting data after select
174
+ this.queryBuilder.addHook('afterSelect', async (result) => {
175
+ if (Array.isArray(result)) {
176
+ return result.map(row => this.decryptData(row));
177
+ } else if (result && typeof result === 'object') {
178
+ return this.decryptData(result);
179
+ }
180
+ return result;
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Generate encryption key
186
+ * @param {number} length - Key length in bytes (default: 32)
187
+ * @returns {Buffer} Generated key
188
+ */
189
+ static generateKey(length = 32) {
190
+ return crypto.randomBytes(length);
191
+ }
192
+
193
+ /**
194
+ * Get plugin metadata
195
+ * @returns {Object} Plugin metadata
196
+ */
197
+ getMetadata() {
198
+ return {
199
+ name: 'EncryptionPlugin',
200
+ version: '1.0.0',
201
+ description: 'Provides AES-256-GCM encryption for sensitive data fields',
202
+ dependencies: ['crypto'],
203
+ features: [
204
+ 'Field-level encryption',
205
+ 'AES-256-GCM algorithm',
206
+ 'Automatic encryption/decryption hooks',
207
+ 'Key management'
208
+ ]
209
+ };
210
+ }
211
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/FullTextSearchPlugin
6
+ */
7
+ import { BasePlugin } from "./BasePlugin.js";
8
+
9
+ /**
10
+ * 全文搜索插件 - 封装不同数据库的全文搜索语法
11
+ */
12
+ export class FullTextSearchPlugin extends BasePlugin {
13
+ constructor(queryBuilder) {
14
+ super(queryBuilder);
15
+ this.pluginName = "FullTextSearchPlugin";
16
+ }
17
+
18
+ _registerMethods() {
19
+ // 注册全文搜索方法到 QueryBuilder
20
+ this.queryBuilder.fullTextSearch = this.fullTextSearch.bind(this);
21
+ this.queryBuilder.orderByRelevance = this.orderByRelevance.bind(this);
22
+ this.queryBuilder.matchAgainst = this.matchAgainst.bind(this);
23
+ this.queryBuilder.fullTextBoolean = this.fullTextBoolean.bind(this);
24
+ this.queryBuilder.fullTextQueryExpansion = this.fullTextQueryExpansion.bind(this);
25
+ }
26
+
27
+ /**
28
+ * 全文搜索
29
+ * @param {string|Array} columns - 要搜索的列
30
+ * @param {string} searchTerm - 搜索词
31
+ * @param {string} mode - 搜索模式 (natural, boolean)
32
+ * @returns {QueryBuilder} QueryBuilder 实例
33
+ */
34
+ fullTextSearch(columns, searchTerm, mode = "natural") {
35
+ this.queryBuilder.query.fullTextSearch = {
36
+ columns: Array.isArray(columns) ? columns : [columns],
37
+ searchTerm,
38
+ mode: mode.toLowerCase(),
39
+ };
40
+ return this.queryBuilder;
41
+ }
42
+
43
+ /**
44
+ * 按相关性排序(全文搜索)
45
+ * @param {string|Array} columns - 用于相关性的列
46
+ * @param {string} searchTerm - 搜索词
47
+ * @returns {QueryBuilder} QueryBuilder 实例
48
+ */
49
+ orderByRelevance(columns, searchTerm) {
50
+ this.queryBuilder.query.orderByRelevance = { columns, searchTerm };
51
+ return this.queryBuilder;
52
+ }
53
+
54
+ /**
55
+ * 使用 MATCH AGAINST 进行全文搜索
56
+ * @param {string|Array} columns - 要搜索的列
57
+ * @param {string} searchTerm - 搜索词
58
+ * @param {string} mode - 搜索模式 (natural, boolean, query expansion)
59
+ * @param {boolean} withQueryExpansion - 是否使用查询扩展
60
+ * @returns {QueryBuilder} QueryBuilder 实例
61
+ */
62
+ matchAgainst(columns, searchTerm, mode = "natural", withQueryExpansion = false) {
63
+ const columnList = Array.isArray(columns)
64
+ ? columns.map((col) => this.queryBuilder.wrapColumn(col)).join(", ")
65
+ : this.queryBuilder.wrapColumn(columns);
66
+
67
+ let modeClause = "";
68
+ if (mode === "boolean") {
69
+ modeClause = "IN BOOLEAN MODE";
70
+ } else if (mode === "query expansion" || withQueryExpansion) {
71
+ modeClause = "WITH QUERY EXPANSION";
72
+ }
73
+
74
+ this.queryBuilder.query.where.push({
75
+ type: "fulltext",
76
+ raw: `MATCH(${columnList}) AGAINST(? ${modeClause})`,
77
+ boolean: "and",
78
+ });
79
+
80
+ this.queryBuilder.bindings.push(searchTerm);
81
+ return this.queryBuilder;
82
+ }
83
+
84
+ /**
85
+ * 布尔模式全文搜索
86
+ * @param {string|Array} columns - 要搜索的列
87
+ * @param {string} searchTerm - 带有布尔运算符的搜索词
88
+ * @returns {QueryBuilder} QueryBuilder 实例
89
+ */
90
+ fullTextBoolean(columns, searchTerm) {
91
+ return this.matchAgainst(columns, searchTerm, "boolean");
92
+ }
93
+
94
+ /**
95
+ * 查询扩展全文搜索
96
+ * @param {string|Array} columns - 要搜索的列
97
+ * @param {string} searchTerm - 搜索词
98
+ * @returns {QueryBuilder} QueryBuilder 实例
99
+ */
100
+ fullTextQueryExpansion(columns, searchTerm) {
101
+ return this.matchAgainst(columns, searchTerm, "query expansion", true);
102
+ }
103
+
104
+ /**
105
+ * 构建全文搜索 SQL
106
+ * @param {Object} fullTextSearch - 全文搜索配置
107
+ * @returns {string} SQL 片段
108
+ */
109
+ buildFullTextSearchSQL(fullTextSearch) {
110
+ const { columns, searchTerm, mode } = fullTextSearch;
111
+ const columnList = Array.isArray(columns)
112
+ ? columns.map((col) => this.queryBuilder.wrapColumn(col)).join(", ")
113
+ : this.queryBuilder.wrapColumn(columns);
114
+
115
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
116
+ let modeClause = "";
117
+ if (mode === "boolean") {
118
+ modeClause = "IN BOOLEAN MODE";
119
+ } else if (mode === "query expansion") {
120
+ modeClause = "WITH QUERY EXPANSION";
121
+ }
122
+
123
+ return `MATCH(${columnList}) AGAINST(? ${modeClause})`;
124
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
125
+ const searchVector = Array.isArray(columns)
126
+ ? columns.map((col) => `to_tsvector(${col})`).join(" || ")
127
+ : `to_tsvector(${this.queryBuilder.wrapColumn(columns)})`;
128
+
129
+ return `${searchVector} @@ plainto_tsquery(?)`;
130
+ } else {
131
+ // 对于不支持全文搜索的数据库,使用 LIKE 模式
132
+ const columnArray = Array.isArray(columns) ? columns : [columns];
133
+ const likeConditions = columnArray.map((col) => {
134
+ return `${this.queryBuilder.wrapColumn(col)} LIKE ?`;
135
+ });
136
+
137
+ return `(${likeConditions.join(" OR ")})`;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 构建相关性排序 SQL
143
+ * @param {Object} orderByRelevance - 相关性排序配置
144
+ * @returns {string} SQL 片段
145
+ */
146
+ buildOrderByRelevanceSQL(orderByRelevance) {
147
+ const { columns, searchTerm } = orderByRelevance;
148
+ const columnList = Array.isArray(columns)
149
+ ? columns.map((col) => this.queryBuilder.wrapColumn(col)).join(", ")
150
+ : this.queryBuilder.wrapColumn(columns);
151
+
152
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
153
+ return `MATCH(${columnList}) AGAINST(?) DESC`;
154
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
155
+ const searchVector = Array.isArray(columns)
156
+ ? columns.map((col) => `to_tsvector(${col})`).join(" || ")
157
+ : `to_tsvector(${this.queryBuilder.wrapColumn(columns)})`;
158
+
159
+ return `ts_rank(${searchVector}, plainto_tsquery(?)) DESC`;
160
+ } else {
161
+ return null; // 不支持相关性排序
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/GeospatialPlugin
6
+ */
7
+ import { BasePlugin } from "./BasePlugin.js";
8
+
9
+
10
+ export class GeospatialPlugin extends BasePlugin {
11
+ constructor(queryBuilder) {
12
+ super(queryBuilder);
13
+ this.pluginName = "GeospatialPlugin";
14
+ }
15
+
16
+ _registerMethods() {
17
+ // 注册地理空间查询方法到 QueryBuilder
18
+ this.queryBuilder.whereDistance = this.whereDistance.bind(this);
19
+ this.queryBuilder.whereWithin = this.whereWithin.bind(this);
20
+ this.queryBuilder.whereIntersects = this.whereIntersects.bind(this);
21
+ this.queryBuilder.orderByDistance = this.orderByDistance.bind(this);
22
+ this.queryBuilder.selectDistance = this.selectDistance.bind(this);
23
+ }
24
+
25
+ /**
26
+ * WHERE 距离条件(适用于 MySQL/PostgreSQL)
27
+ * @param {string} column - 几何列名
28
+ * @param {Object} point - 点坐标 {latitude, longitude} 或 {x, y}
29
+ * @param {number} distance - 距离(米)
30
+ * @param {string} operator - 比较运算符 (<, <=, >, >=, =)
31
+ * @returns {QueryBuilder} QueryBuilder 实例
32
+ */
33
+ whereDistance(column, point, distance, operator = "<") {
34
+ const { latitude, longitude, x, y } = point;
35
+ const lat = latitude !== undefined ? latitude : y;
36
+ const lng = longitude !== undefined ? longitude : x;
37
+
38
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
39
+ // MySQL 空间扩展
40
+ this.queryBuilder.whereRaw(
41
+ `ST_Distance_Sphere(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?)) ${operator} ?`,
42
+ [`POINT(${lng} ${lat})`, distance]
43
+ );
44
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
45
+ // PostgreSQL PostGIS
46
+ this.queryBuilder.whereRaw(
47
+ `ST_Distance(${this.queryBuilder.wrapColumn(column)}::geography, ST_MakePoint(?, ?)::geography) ${operator} ?`,
48
+ [lng, lat, distance]
49
+ );
50
+ }
51
+
52
+ return this.queryBuilder;
53
+ }
54
+
55
+ /**
56
+ * WHERE 在多边形内条件
57
+ * @param {string} column - 几何列名
58
+ * @param {Array} polygon - 点数组 [{latitude, longitude}, ...]
59
+ * @returns {QueryBuilder} QueryBuilder 实例
60
+ */
61
+ whereWithin(column, polygon) {
62
+ const points = polygon
63
+ .map((p) => {
64
+ const { latitude, longitude, x, y } = p;
65
+ const lat = latitude !== undefined ? latitude : y;
66
+ const lng = longitude !== undefined ? longitude : x;
67
+ return `${lng} ${lat}`;
68
+ })
69
+ .join(", ");
70
+
71
+ const polygonWkt = `POLYGON((${points}))`;
72
+
73
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
74
+ this.queryBuilder.whereRaw(
75
+ `ST_Within(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?))`,
76
+ [polygonWkt]
77
+ );
78
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
79
+ this.queryBuilder.whereRaw(
80
+ `ST_Within(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?, 4326))`,
81
+ [polygonWkt]
82
+ );
83
+ }
84
+
85
+ return this.queryBuilder;
86
+ }
87
+
88
+ /**
89
+ * WHERE 与几何图形相交条件
90
+ * @param {string} column - 几何列名
91
+ * @param {Object} geometry - 几何对象
92
+ * @returns {QueryBuilder} QueryBuilder 实例
93
+ */
94
+ whereIntersects(column, geometry) {
95
+ let geometryWkt;
96
+
97
+ if (geometry.type === "point") {
98
+ const { latitude, longitude, x, y } = geometry;
99
+ const lat = latitude !== undefined ? latitude : y;
100
+ const lng = longitude !== undefined ? longitude : x;
101
+ geometryWkt = `POINT(${lng} ${lat})`;
102
+ } else if (geometry.type === "polygon") {
103
+ const points = geometry.coordinates
104
+ .map((p) => {
105
+ const { latitude, longitude, x, y } = p;
106
+ const lat = latitude !== undefined ? latitude : y;
107
+ const lng = longitude !== undefined ? longitude : x;
108
+ return `${lng} ${lat}`;
109
+ })
110
+ .join(", ");
111
+ geometryWkt = `POLYGON((${points}))`;
112
+ } else if (geometry.type === "linestring") {
113
+ const points = geometry.coordinates
114
+ .map((p) => {
115
+ const { latitude, longitude, x, y } = p;
116
+ const lat = latitude !== undefined ? latitude : y;
117
+ const lng = longitude !== undefined ? longitude : x;
118
+ return `${lng} ${lat}`;
119
+ })
120
+ .join(", ");
121
+ geometryWkt = `LINESTRING(${points})`;
122
+ }
123
+
124
+ if (geometryWkt) {
125
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
126
+ this.queryBuilder.whereRaw(
127
+ `ST_Intersects(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?))`,
128
+ [geometryWkt]
129
+ );
130
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
131
+ this.queryBuilder.whereRaw(
132
+ `ST_Intersects(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?, 4326))`,
133
+ [geometryWkt]
134
+ );
135
+ }
136
+ }
137
+
138
+ return this.queryBuilder;
139
+ }
140
+
141
+ /**
142
+ * 按距离排序
143
+ * @param {string} column - 几何列名
144
+ * @param {Object} point - 点坐标
145
+ * @param {string} direction - 排序方向 (asc, desc)
146
+ * @returns {QueryBuilder} QueryBuilder 实例
147
+ */
148
+ orderByDistance(column, point, direction = "asc") {
149
+ const { latitude, longitude, x, y } = point;
150
+ const lat = latitude !== undefined ? latitude : y;
151
+ const lng = longitude !== undefined ? longitude : x;
152
+
153
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
154
+ this.queryBuilder.orderByRaw(
155
+ `ST_Distance_Sphere(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?)) ${direction.toUpperCase()}`,
156
+ [`POINT(${lng} ${lat})`]
157
+ );
158
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
159
+ this.queryBuilder.orderByRaw(
160
+ `ST_Distance(${this.queryBuilder.wrapColumn(column)}::geography, ST_MakePoint(?, ?)::geography) ${direction.toUpperCase()}`,
161
+ [lng, lat]
162
+ );
163
+ }
164
+
165
+ return this.queryBuilder;
166
+ }
167
+
168
+ /**
169
+ * 在 SELECT 中计算距离
170
+ * @param {string} column - 几何列名
171
+ * @param {Object} point - 点坐标
172
+ * @param {string} alias - 列别名
173
+ * @returns {QueryBuilder} QueryBuilder 实例
174
+ */
175
+ selectDistance(column, point, alias = "distance") {
176
+ const { latitude, longitude, x, y } = point;
177
+ const lat = latitude !== undefined ? latitude : y;
178
+ const lng = longitude !== undefined ? longitude : x;
179
+
180
+ if (["mysql", "mariadb"].includes(this.queryBuilder.dialect)) {
181
+ this.queryBuilder.selectRaw(
182
+ `ST_Distance_Sphere(${this.queryBuilder.wrapColumn(column)}, ST_GeomFromText(?)) as ${alias}`,
183
+ [`POINT(${lng} ${lat})`]
184
+ );
185
+ } else if (["postgresql", "postgres", "pg"].includes(this.queryBuilder.dialect)) {
186
+ this.queryBuilder.selectRaw(
187
+ `ST_Distance(${this.queryBuilder.wrapColumn(column)}::geography, ST_MakePoint(?, ?)::geography) as ${alias}`,
188
+ [lng, lat]
189
+ );
190
+ }
191
+
192
+ return this.queryBuilder;
193
+ }
194
+
195
+ /**
196
+ * 获取支持的地理空间函数
197
+ * @returns {Array} 支持的函数列表
198
+ */
199
+ getSupportedFunctions() {
200
+ const functions = {
201
+ mysql: ["ST_Distance_Sphere", "ST_Within", "ST_Intersects", "ST_GeomFromText"],
202
+ mariadb: ["ST_Distance_Sphere", "ST_Within", "ST_Intersects", "ST_GeomFromText"],
203
+ postgresql: ["ST_Distance", "ST_Within", "ST_Intersects", "ST_MakePoint", "ST_GeomFromText"],
204
+ postgres: ["ST_Distance", "ST_Within", "ST_Intersects", "ST_MakePoint", "ST_GeomFromText"],
205
+ pg: ["ST_Distance", "ST_Within", "ST_Intersects", "ST_MakePoint", "ST_GeomFromText"],
206
+ };
207
+
208
+ return functions[this.queryBuilder.dialect] || [];
209
+ }
210
+
211
+ /**
212
+ * 检查数据库是否支持地理空间查询
213
+ * @returns {boolean} 是否支持
214
+ */
215
+ isGeospatialSupported() {
216
+ const supportedDialects = ["mysql", "mariadb", "postgresql", "postgres", "pg"];
217
+ return supportedDialects.includes(this.queryBuilder.dialect);
218
+ }
219
+ }