velocious 1.0.566 → 1.0.568

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 (110) hide show
  1. package/README.md +31 -1
  2. package/bin/velocious.js +28 -1
  3. package/build/bin/velocious.js +28 -1
  4. package/build/database/drivers/base-table.js +4 -3
  5. package/build/database/drivers/base.js +28 -0
  6. package/build/database/drivers/index-metadata.js +28 -0
  7. package/build/database/drivers/mssql/column.js +2 -34
  8. package/build/database/drivers/mssql/columns-index.js +37 -0
  9. package/build/database/drivers/mssql/table.js +55 -11
  10. package/build/database/drivers/mysql/sql/alter-table.js +13 -0
  11. package/build/database/drivers/pgsql/column.js +2 -27
  12. package/build/database/drivers/pgsql/columns-index.js +37 -0
  13. package/build/database/drivers/pgsql/table.js +61 -10
  14. package/build/database/drivers/sqlite/sql/alter-table.js +18 -0
  15. package/build/database/migration/index.js +92 -3
  16. package/build/database/query/alter-table-base.js +20 -1
  17. package/build/database/record/attachments/normalize-input.js +38 -10
  18. package/build/database/record/attachments/storage-drivers/filesystem.js +33 -2
  19. package/build/database/record/attachments/storage-drivers/native.js +17 -2
  20. package/build/database/record/attachments/storage-drivers/s3.js +27 -7
  21. package/build/database/record/attachments/store.js +159 -43
  22. package/build/database/table-data/table-foreign-key.js +9 -1
  23. package/build/environment-handlers/base.js +1 -1
  24. package/build/environment-handlers/node/attachment-path-source.js +144 -0
  25. package/build/environment-handlers/node.js +28 -3
  26. package/build/src/database/drivers/base-table.d.ts.map +1 -1
  27. package/build/src/database/drivers/base-table.js +4 -3
  28. package/build/src/database/drivers/base.d.ts +7 -0
  29. package/build/src/database/drivers/base.d.ts.map +1 -1
  30. package/build/src/database/drivers/base.js +24 -1
  31. package/build/src/database/drivers/index-metadata.d.ts +41 -0
  32. package/build/src/database/drivers/index-metadata.d.ts.map +1 -0
  33. package/build/src/database/drivers/index-metadata.js +26 -0
  34. package/build/src/database/drivers/mssql/column.d.ts.map +1 -1
  35. package/build/src/database/drivers/mssql/column.js +3 -31
  36. package/build/src/database/drivers/mssql/columns-index.d.ts +41 -0
  37. package/build/src/database/drivers/mssql/columns-index.d.ts.map +1 -1
  38. package/build/src/database/drivers/mssql/columns-index.js +35 -1
  39. package/build/src/database/drivers/mssql/table.d.ts +42 -0
  40. package/build/src/database/drivers/mssql/table.d.ts.map +1 -1
  41. package/build/src/database/drivers/mssql/table.js +49 -12
  42. package/build/src/database/drivers/mysql/sql/alter-table.d.ts.map +1 -1
  43. package/build/src/database/drivers/mysql/sql/alter-table.js +12 -1
  44. package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
  45. package/build/src/database/drivers/pgsql/column.js +3 -24
  46. package/build/src/database/drivers/pgsql/columns-index.d.ts +41 -0
  47. package/build/src/database/drivers/pgsql/columns-index.d.ts.map +1 -1
  48. package/build/src/database/drivers/pgsql/columns-index.js +35 -1
  49. package/build/src/database/drivers/pgsql/table.d.ts +42 -0
  50. package/build/src/database/drivers/pgsql/table.d.ts.map +1 -1
  51. package/build/src/database/drivers/pgsql/table.js +54 -11
  52. package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
  53. package/build/src/database/drivers/sqlite/sql/alter-table.js +15 -1
  54. package/build/src/database/migration/index.d.ts +35 -1
  55. package/build/src/database/migration/index.d.ts.map +1 -1
  56. package/build/src/database/migration/index.js +78 -4
  57. package/build/src/database/query/alter-table-base.d.ts +6 -0
  58. package/build/src/database/query/alter-table-base.d.ts.map +1 -1
  59. package/build/src/database/query/alter-table-base.js +18 -2
  60. package/build/src/database/record/attachments/normalize-input.d.ts +33 -4
  61. package/build/src/database/record/attachments/normalize-input.d.ts.map +1 -1
  62. package/build/src/database/record/attachments/normalize-input.js +39 -11
  63. package/build/src/database/record/attachments/storage-drivers/filesystem.d.ts +2 -5
  64. package/build/src/database/record/attachments/storage-drivers/filesystem.d.ts.map +1 -1
  65. package/build/src/database/record/attachments/storage-drivers/filesystem.js +28 -3
  66. package/build/src/database/record/attachments/storage-drivers/native.d.ts +2 -6
  67. package/build/src/database/record/attachments/storage-drivers/native.d.ts.map +1 -1
  68. package/build/src/database/record/attachments/storage-drivers/native.js +14 -3
  69. package/build/src/database/record/attachments/storage-drivers/s3.d.ts +2 -6
  70. package/build/src/database/record/attachments/storage-drivers/s3.d.ts.map +1 -1
  71. package/build/src/database/record/attachments/storage-drivers/s3.js +29 -8
  72. package/build/src/database/record/attachments/store.d.ts +27 -0
  73. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  74. package/build/src/database/record/attachments/store.js +144 -44
  75. package/build/src/database/table-data/table-foreign-key.d.ts +9 -1
  76. package/build/src/database/table-data/table-foreign-key.d.ts.map +1 -1
  77. package/build/src/database/table-data/table-foreign-key.js +9 -2
  78. package/build/src/environment-handlers/base.d.ts +2 -5
  79. package/build/src/environment-handlers/base.d.ts.map +1 -1
  80. package/build/src/environment-handlers/base.js +2 -2
  81. package/build/src/environment-handlers/node/attachment-path-source.d.ts +64 -0
  82. package/build/src/environment-handlers/node/attachment-path-source.d.ts.map +1 -0
  83. package/build/src/environment-handlers/node/attachment-path-source.js +116 -0
  84. package/build/src/environment-handlers/node.d.ts +12 -0
  85. package/build/src/environment-handlers/node.d.ts.map +1 -1
  86. package/build/src/environment-handlers/node.js +24 -4
  87. package/build/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +1 -1
  89. package/src/database/drivers/base-table.js +4 -3
  90. package/src/database/drivers/base.js +28 -0
  91. package/src/database/drivers/index-metadata.js +28 -0
  92. package/src/database/drivers/mssql/column.js +2 -34
  93. package/src/database/drivers/mssql/columns-index.js +37 -0
  94. package/src/database/drivers/mssql/table.js +55 -11
  95. package/src/database/drivers/mysql/sql/alter-table.js +13 -0
  96. package/src/database/drivers/pgsql/column.js +2 -27
  97. package/src/database/drivers/pgsql/columns-index.js +37 -0
  98. package/src/database/drivers/pgsql/table.js +61 -10
  99. package/src/database/drivers/sqlite/sql/alter-table.js +18 -0
  100. package/src/database/migration/index.js +92 -3
  101. package/src/database/query/alter-table-base.js +20 -1
  102. package/src/database/record/attachments/normalize-input.js +38 -10
  103. package/src/database/record/attachments/storage-drivers/filesystem.js +33 -2
  104. package/src/database/record/attachments/storage-drivers/native.js +17 -2
  105. package/src/database/record/attachments/storage-drivers/s3.js +27 -7
  106. package/src/database/record/attachments/store.js +159 -43
  107. package/src/database/table-data/table-foreign-key.js +9 -1
  108. package/src/environment-handlers/base.js +1 -1
  109. package/src/environment-handlers/node/attachment-path-source.js +144 -0
  110. package/src/environment-handlers/node.js +28 -3
package/README.md CHANGED
@@ -37,6 +37,7 @@
37
37
  * AbortSignal-driven MySQL/MariaDB query cancellation for raw, model, and cross-tenant aggregate queries (see [docs/database-query-cancellation.md](docs/database-query-cancellation.md))
38
38
  * Optional built-in debug endpoint for inspecting server and database connection state (see [docs/debug-endpoint.md](docs/debug-endpoint.md))
39
39
  * Optional built-in API manifest endpoint describing every registered frontend-model resource as human- and machine-readable JSON (see [docs/api-manifest-endpoint.md](docs/api-manifest-endpoint.md))
40
+ * Backend record attachments with filesystem, S3, native callback, and bounded Node path-input persistence (see [docs/attachments.md](docs/attachments.md))
40
41
 
41
42
  # Setup
42
43
 
@@ -521,6 +522,9 @@ Task.hasOneAttachment("descriptionFile")
521
522
  Task.hasOneAttachment("archivedPdf", {driver: "s3"})
522
523
  ```
523
524
 
525
+ See [Backend record attachments](docs/attachments.md) for the complete input,
526
+ storage-driver, lifecycle, and path-security contracts.
527
+
524
528
  You can also pass a driver class or instance directly on the attachment:
525
529
 
526
530
  ```js
@@ -538,6 +542,10 @@ await task.descriptionFile().attach({
538
542
  content: "my file content",
539
543
  filename: "file.doc"
540
544
  })
545
+ await task.archivedPdf().attach({
546
+ path: "/var/app/uploads/archive.pdf",
547
+ contentType: "application/pdf"
548
+ })
541
549
  const descriptionFileUrl = await task.descriptionFile().url()
542
550
  await task.update({
543
551
  descriptionFile: {
@@ -605,7 +613,25 @@ new Configuration({
605
613
  ```
606
614
 
607
615
  Then `{path: "..."}`
608
- inputs are only accepted when the file resolves inside one of the allowed prefixes.
616
+ inputs are only accepted when the path resolves inside one of the allowed
617
+ prefixes and the once-opened handle identifies a regular file. Its exact byte
618
+ size comes from that handle's stat snapshot. The filesystem driver copies it
619
+ with a bounded, backpressured stream and the S3 driver sends a Node `Readable`;
620
+ on current nullable schemas neither driver first materializes the whole file as
621
+ a Buffer or Base64 value. Legacy schemas with a non-null `content_base64`
622
+ column instead materialize the opened snapshot once before driver persistence
623
+ and reuse those exact bytes for storage and the database Base64. Path
624
+ replacement cannot switch the opened source, truncation is rejected, later
625
+ appends are ignored, and the source is closed after persistence.
626
+
627
+ The native driver's documented `write({contentBase64, ...})` callback remains
628
+ source-compatible. For path input only, that driver reads and Base64-encodes the
629
+ opened source after driver selection on current schemas; legacy path input
630
+ arrives pre-materialized with the same Base64 written to the database. Existing
631
+ Buffer, string, browser, and `UploadedFile` inputs keep their in-memory
632
+ behavior. See
633
+ [docs/attachments.md](docs/attachments.md#normalized-storage-driver-input) for
634
+ the normalized input passed to custom drivers.
609
635
 
610
636
  For frontend models, configure `resourceConfig().attachments` and use:
611
637
 
@@ -1148,6 +1174,8 @@ export default class CreateEvents extends Migration {
1148
1174
  }
1149
1175
  ```
1150
1176
 
1177
+ To reverse an `addReference("tasks", "project", {foreignKey: true, type: "uuid"})` migration, use `await this.removeReference("tasks", "project")` in `down()`. It removes the generated foreign key, index, and column; see [removing references and foreign keys](docs/database-migrations.md#removing-references-and-foreign-keys) for custom-column and foreign-key-only cases.
1178
+
1151
1179
  Migrations that must be rerunnable can guard changes with `tableExists(...)`, `columnExists(table, column)` and `indexExists(table, index)` — a missing table yields `false` rather than throwing. See [docs/database-migrations.md](docs/database-migrations.md#guarding-schema-changes-in-rerunnable-migrations).
1152
1180
 
1153
1181
  ## Run migrations from the command line
@@ -1180,6 +1208,8 @@ Evaluate inline JavaScript (Rails-style runner) with initialized app/database co
1180
1208
  npx velocious runner "const users = await db.query('SELECT COUNT(*) AS count FROM users'); console.log(users[0].count)"
1181
1209
  ```
1182
1210
 
1211
+ Successful CLI commands exit with status `0`. If command execution rejects, Velocious marks the process failed and always runs final database cleanup. Successful cleanup preserves and rethrows the original command error unchanged; if cleanup also fails, an `AggregateError` retains the command error first and as its `cause`, followed by the cleanup error. Cleanup-only failures also exit nonzero. These failures therefore remain nonzero even when application code installs an `uncaughtException` listener that reports and consumes the error. See [CLI process exit behavior](docs/cli.md#process-exit-behavior).
1212
+
1183
1213
  By default, migrations write `db/structure-<identifier>.sql` files for each database in non-test environments. Test skips these automatic writes unless you explicitly opt in. Configure allow/deny lists in your configuration:
1184
1214
 
1185
1215
  ```js
package/bin/velocious.js CHANGED
@@ -39,10 +39,37 @@ const cli = new Cli({
39
39
  processArgs
40
40
  })
41
41
 
42
+ let commandError
43
+ let commandFailed = false
44
+
42
45
  try {
43
46
  await cli.execute()
44
- } finally {
47
+ } catch (error) {
48
+ commandError = error
49
+ commandFailed = true
50
+ process.exitCode = 1
51
+ }
52
+
53
+ let cleanupError
54
+ let cleanupFailed = false
55
+
56
+ try {
45
57
  await configuration.closeDatabaseConnections()
58
+ } catch (error) {
59
+ cleanupError = error
60
+ cleanupFailed = true
61
+ process.exitCode = 1
62
+ }
63
+
64
+ if (commandFailed && cleanupFailed) {
65
+ throw new AggregateError(
66
+ [commandError, cleanupError],
67
+ "Velocious CLI command execution and database cleanup both failed",
68
+ {cause: commandError}
69
+ )
46
70
  }
47
71
 
72
+ if (commandFailed) throw commandError
73
+ if (cleanupFailed) throw cleanupError
74
+
48
75
  process.exit(0)
@@ -39,10 +39,37 @@ const cli = new Cli({
39
39
  processArgs
40
40
  })
41
41
 
42
+ let commandError
43
+ let commandFailed = false
44
+
42
45
  try {
43
46
  await cli.execute()
44
- } finally {
47
+ } catch (error) {
48
+ commandError = error
49
+ commandFailed = true
50
+ process.exitCode = 1
51
+ }
52
+
53
+ let cleanupError
54
+ let cleanupFailed = false
55
+
56
+ try {
45
57
  await configuration.closeDatabaseConnections()
58
+ } catch (error) {
59
+ cleanupError = error
60
+ cleanupFailed = true
61
+ process.exitCode = 1
62
+ }
63
+
64
+ if (commandFailed && cleanupFailed) {
65
+ throw new AggregateError(
66
+ [commandError, cleanupError],
67
+ "Velocious CLI command execution and database cleanup both failed",
68
+ {cause: commandError}
69
+ )
46
70
  }
47
71
 
72
+ if (commandFailed) throw commandError
73
+ if (cleanupFailed) throw cleanupError
74
+
48
75
  process.exit(0)
@@ -104,13 +104,15 @@ export default class VelociousDatabaseDriversBaseTable {
104
104
  }
105
105
 
106
106
  for (const foreignKey of await this.getForeignKeys()) {
107
- tableData.addForeignKey(foreignKey.getTableDataForeignKey())
107
+ const tableDataForeignKey = foreignKey.getTableDataForeignKey()
108
+
109
+ tableData.addForeignKey(tableDataForeignKey)
108
110
 
109
111
  const tableDataColumn = tableDataColumns.find((tableDataColumn) => tableDataColumn.getName() == foreignKey.getColumnName())
110
112
 
111
113
  if (!tableDataColumn) throw new Error(`Couldn't find table data column for foreign key: ${foreignKey.getColumnName()}`)
112
114
 
113
- tableDataColumn.setForeignKey(foreignKey)
115
+ tableDataColumn.setForeignKey(tableDataForeignKey)
114
116
  }
115
117
 
116
118
  for (const index of await this.getIndexes()) {
@@ -153,4 +155,3 @@ export default class VelociousDatabaseDriversBaseTable {
153
155
  return await this.getDriver().query(sql)
154
156
  }
155
157
  }
156
-
@@ -224,6 +224,34 @@ export default class VelociousDatabaseDriversBase {
224
224
  }
225
225
  }
226
226
 
227
+ /**
228
+ * Runs remove foreign key.
229
+ * @param {string} tableName - Table name.
230
+ * @param {import("./base-foreign-key.js").default} foreignKeyMetadata - Foreign key metadata.
231
+ * @returns {Promise<void>} - Resolves when complete.
232
+ */
233
+ async removeForeignKey(tableName, foreignKeyMetadata) {
234
+ this._assertNotReadOnly()
235
+
236
+ const tableForeignKey = new TableForeignKey({
237
+ columnName: foreignKeyMetadata.getColumnName(),
238
+ dropForeignKey: true,
239
+ name: foreignKeyMetadata.getName(),
240
+ referencedColumnName: foreignKeyMetadata.getReferencedColumnName(),
241
+ referencedTableName: foreignKeyMetadata.getReferencedTableName(),
242
+ tableName
243
+ })
244
+ const tableData = new TableData(tableName)
245
+
246
+ tableData.addForeignKey(tableForeignKey)
247
+
248
+ const alterTableSQLs = await this.alterTableSQLs(tableData)
249
+
250
+ for (const alterTableSQL of alterTableSQLs) {
251
+ await this.query(alterTableSQL)
252
+ }
253
+ }
254
+
227
255
  /**
228
256
  * Runs alter table sqls.
229
257
  * @abstract
@@ -0,0 +1,28 @@
1
+ // @ts-check
2
+
3
+ import { forcedBoolean, forcedString } from "typanic"
4
+
5
+ /**
6
+ * IndexMetadataType type.
7
+ * @typedef {object} IndexMetadataType
8
+ * @property {string} column_name - Index column name.
9
+ * @property {string} index_name - Index name.
10
+ * @property {boolean} is_primary_key - Whether the index is primary.
11
+ * @property {boolean} is_unique - Whether the index is unique.
12
+ * @property {string} table_name - Table name.
13
+ */
14
+
15
+ /**
16
+ * Normalizes one untrusted database index metadata row.
17
+ * @param {import("./base.js").QueryRowType} row - Database index metadata row.
18
+ * @returns {IndexMetadataType} - Validated index metadata.
19
+ */
20
+ export function normalizeIndexMetadataRow(row) {
21
+ return {
22
+ column_name: forcedString(row.column_name, "index column_name"),
23
+ index_name: forcedString(row.index_name, "index index_name"),
24
+ is_primary_key: forcedBoolean(row.is_primary_key, "index is_primary_key"),
25
+ is_unique: forcedBoolean(row.is_unique, "index is_unique"),
26
+ table_name: forcedString(row.table_name, "index table_name")
27
+ }
28
+ }
@@ -1,7 +1,6 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseColumn from "../base-column.js"
4
- import ColumnsIndex from "./columns-index.js"
5
4
  import {digg} from "diggerize"
6
5
 
7
6
  export default class VelociousDatabaseDriversMssqlColumn extends BaseColumn {
@@ -19,39 +18,9 @@ export default class VelociousDatabaseDriversMssqlColumn extends BaseColumn {
19
18
  getAutoIncrement() { return digg(this, "data", "isIdentity") === 1 }
20
19
 
21
20
  async getIndexes() {
22
- const options = this.getOptions()
23
- const sql = `
24
- SELECT
25
- sys.tables.name AS TableName,
26
- sys.columns.name AS ColumnName,
27
- sys.indexes.name AS index_name,
28
- sys.indexes.type_desc AS IndexType,
29
- sys.index_columns.is_included_column AS IsIncludedColumn,
30
- sys.indexes.is_unique,
31
- sys.indexes.is_primary_key,
32
- sys.indexes.is_unique_constraint
33
- FROM sys.indexes
34
- INNER JOIN sys.index_columns ON sys.indexes.object_id = sys.index_columns.object_id AND sys.indexes.index_id = sys.index_columns.index_id
35
- INNER JOIN sys.columns ON sys.index_columns.object_id = sys.columns.object_id AND sys.index_columns.column_id = sys.columns.column_id
36
- INNER JOIN sys.tables ON sys.indexes.object_id = sys.tables.object_id
37
- WHERE
38
- sys.tables.name = ${options.quote(this.getTable().getName())} AND
39
- sys.columns.name = ${options.quote(this.getName())}
40
- ORDER BY
41
- sys.indexes.name,
42
- sys.index_columns.key_ordinal
43
- `
21
+ const indexes = await this.getTable().getIndexes()
44
22
 
45
- const rows = await this.getDriver().query(sql)
46
- const indexes = []
47
-
48
- for (const row of rows) {
49
- const index = new ColumnsIndex(this.getTable(), row)
50
-
51
- indexes.push(index)
52
- }
53
-
54
- return indexes
23
+ return indexes.filter((index) => index.getColumnNames().includes(this.getName()))
55
24
  }
56
25
 
57
26
  getDefault() { return digg(this, "data", "COLUMN_DEFAULT") }
@@ -71,4 +40,3 @@ export default class VelociousDatabaseDriversMssqlColumn extends BaseColumn {
71
40
  getPrimaryKey() { return digg(this, "data", "isIdentity") === 1 }
72
41
  getType() { return digg(this, "data", "DATA_TYPE") }
73
42
  }
74
-
@@ -1,6 +1,43 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseColumnsIndex from "../base-columns-index.js"
4
+ import TableIndex from "../../table-data/table-index.js"
5
+
6
+ /**
7
+ * MssqlColumnsIndexDataType type.
8
+ * @typedef {object} MssqlColumnsIndexDataType
9
+ * @property {string[]} columnNames - Ordered index column names.
10
+ * @property {string} index_name - Index name.
11
+ * @property {boolean} is_primary_key - Whether the index is primary.
12
+ * @property {boolean} is_unique - Whether the index is unique.
13
+ * @property {string} table_name - Table name.
14
+ */
4
15
 
5
16
  export default class VelociousDatabaseDriversMssqlColumnsIndex extends BaseColumnsIndex {
17
+ /**
18
+ * Runs constructor.
19
+ * @param {import("../base-table.js").default} table - Table.
20
+ * @param {MssqlColumnsIndexDataType} data - Grouped index metadata.
21
+ */
22
+ constructor(table, data) {
23
+ super(table, data)
24
+ this.indexData = data
25
+ }
26
+
27
+ /**
28
+ * Runs get column names.
29
+ * @returns {string[]} - Ordered index column names.
30
+ */
31
+ getColumnNames() { return this.indexData.columnNames }
32
+
33
+ /**
34
+ * Runs get table data index.
35
+ * @returns {TableIndex} - Table-data index.
36
+ */
37
+ getTableDataIndex() {
38
+ return new TableIndex(this.getColumnNames(), {
39
+ name: this.getName(),
40
+ unique: this.isUnique()
41
+ })
42
+ }
6
43
  }
@@ -5,6 +5,51 @@ import Column from "./column.js"
5
5
  import ColumnsIndex from "./columns-index.js"
6
6
  import {digg} from "diggerize"
7
7
  import ForeignKey from "./foreign-key.js"
8
+ import { normalizeIndexMetadataRow } from "../index-metadata.js"
9
+
10
+ /**
11
+ * MssqlGroupedIndexDataType type.
12
+ * @typedef {object} MssqlGroupedIndexDataType
13
+ * @property {string[]} columnNames - Ordered index column names.
14
+ * @property {string} index_name - Index name.
15
+ * @property {boolean} is_primary_key - Whether the index is primary.
16
+ * @property {boolean} is_unique - Whether the index is unique.
17
+ * @property {string} table_name - Table name.
18
+ */
19
+
20
+ /**
21
+ * Groups ordered SQL Server index rows into one metadata value per index.
22
+ * @param {import("../index-metadata.js").IndexMetadataType[]} indexRows - Ordered index metadata rows.
23
+ * @returns {MssqlGroupedIndexDataType[]} - Grouped index metadata.
24
+ */
25
+ export function groupMssqlIndexRows(indexRows) {
26
+ /** @type {Map<string, MssqlGroupedIndexDataType>} */
27
+ const indexDataByName = new Map()
28
+ /** @type {MssqlGroupedIndexDataType[]} */
29
+ const groupedIndexData = []
30
+
31
+ for (const indexRow of indexRows) {
32
+ const existingIndexData = indexDataByName.get(indexRow.index_name)
33
+
34
+ if (existingIndexData) {
35
+ existingIndexData.columnNames.push(indexRow.column_name)
36
+ continue
37
+ }
38
+
39
+ const indexData = {
40
+ columnNames: [indexRow.column_name],
41
+ index_name: indexRow.index_name,
42
+ is_primary_key: indexRow.is_primary_key,
43
+ is_unique: indexRow.is_unique,
44
+ table_name: indexRow.table_name
45
+ }
46
+
47
+ indexDataByName.set(indexRow.index_name, indexData)
48
+ groupedIndexData.push(indexData)
49
+ }
50
+
51
+ return groupedIndexData
52
+ }
8
53
 
9
54
  export default class VelociousDatabaseDriversMssqlTable extends BaseTable {
10
55
  /**
@@ -43,7 +88,7 @@ export default class VelociousDatabaseDriversMssqlTable extends BaseTable {
43
88
  return await this.getDriver()._cachedTableSchemaMetadata(this.getName(), "foreignKeys", async () => {
44
89
  const sql = `
45
90
  SELECT
46
- fk.name AS ForeignKeyName,
91
+ fk.name AS CONSTRAINT_NAME,
47
92
  tp.name AS ParentTable,
48
93
  ref.name AS ReferencedTable,
49
94
  cp.name AS ParentColumn,
@@ -63,7 +108,7 @@ export default class VelociousDatabaseDriversMssqlTable extends BaseTable {
63
108
  ON fkc.referenced_object_id = cref.object_id
64
109
  AND fkc.referenced_column_id = cref.column_id
65
110
  WHERE tp.name = ${this.driver.quote(this.getName())}
66
- ORDER BY ForeignKeyName, ParentTable, ReferencedTable;
111
+ ORDER BY CONSTRAINT_NAME, ParentTable, ReferencedTable;
67
112
  `
68
113
 
69
114
  const foreignKeyRows = await this.driver.query(sql)
@@ -84,20 +129,18 @@ export default class VelociousDatabaseDriversMssqlTable extends BaseTable {
84
129
  const options = this.getOptions()
85
130
  const sql = `
86
131
  SELECT
87
- sys.tables.name AS TableName,
88
- sys.columns.name AS ColumnName,
132
+ sys.tables.name AS table_name,
133
+ sys.columns.name AS column_name,
89
134
  sys.indexes.name AS index_name,
90
- sys.indexes.type_desc AS IndexType,
91
- sys.index_columns.is_included_column AS IsIncludedColumn,
92
135
  sys.indexes.is_unique,
93
- sys.indexes.is_primary_key,
94
- sys.indexes.is_unique_constraint
136
+ sys.indexes.is_primary_key
95
137
  FROM sys.indexes
96
138
  INNER JOIN sys.index_columns ON sys.indexes.object_id = sys.index_columns.object_id AND sys.indexes.index_id = sys.index_columns.index_id
97
139
  INNER JOIN sys.columns ON sys.index_columns.object_id = sys.columns.object_id AND sys.index_columns.column_id = sys.columns.column_id
98
140
  INNER JOIN sys.tables ON sys.indexes.object_id = sys.tables.object_id
99
141
  WHERE
100
- sys.tables.name = ${options.quote(this.getName())}
142
+ sys.tables.name = ${options.quote(this.getName())} AND
143
+ sys.index_columns.is_included_column = 0
101
144
  ORDER BY
102
145
  sys.indexes.name,
103
146
  sys.index_columns.key_ordinal
@@ -105,9 +148,10 @@ export default class VelociousDatabaseDriversMssqlTable extends BaseTable {
105
148
 
106
149
  const rows = await this.getDriver().query(sql)
107
150
  const indexes = []
151
+ const indexRows = rows.map((row) => normalizeIndexMetadataRow(row))
108
152
 
109
- for (const row of rows) {
110
- const index = new ColumnsIndex(this, row)
153
+ for (const indexData of groupMssqlIndexRows(indexRows)) {
154
+ const index = new ColumnsIndex(this, indexData)
111
155
 
112
156
  indexes.push(index)
113
157
  }
@@ -4,6 +4,19 @@ import AlterTableBase from "../../../query/alter-table-base.js"
4
4
  import TableColumn from "../../../table-data/table-column.js"
5
5
 
6
6
  export default class VelociousDatabaseConnectionDriversMysqlSqlAlterTable extends AlterTableBase {
7
+ /**
8
+ * Runs get drop foreign key sql.
9
+ * @param {import("../../../table-data/table-foreign-key.js").default} foreignKey - Foreign key to drop.
10
+ * @returns {string} - SQL fragment that removes the foreign key.
11
+ */
12
+ getDropForeignKeySQL(foreignKey) {
13
+ const name = foreignKey.getName()
14
+
15
+ if (!name) throw new Error(`Cannot remove unnamed foreign key on ${foreignKey.getTableName()}.${foreignKey.getColumnName()}`)
16
+
17
+ return `DROP FOREIGN KEY ${this.getOptions().quoteIndexName(name)}`
18
+ }
19
+
7
20
  /**
8
21
  * Builds MySQL ALTER TABLE statements, adding indexes atomically with columns.
9
22
  * @returns {Promise<string[]>} - Resolves with SQL statements.
@@ -1,7 +1,6 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseColumn from "../base-column.js"
4
- import ColumnsIndex from "./columns-index.js"
5
4
  import {digg} from "diggerize"
6
5
 
7
6
  export default class VelociousDatabaseDriversPgsqlColumn extends BaseColumn {
@@ -25,33 +24,9 @@ export default class VelociousDatabaseDriversPgsqlColumn extends BaseColumn {
25
24
  }
26
25
 
27
26
  async getIndexes() {
28
- const options = this.getOptions()
27
+ const indexes = await this.getTable().getIndexes()
29
28
 
30
- const indexesRows = await this.table.getDriver().query(`
31
- SELECT
32
- pg_attribute.attname AS column_name,
33
- pg_index.indexrelid::regclass as index_name,
34
- pg_class.relnamespace::regnamespace as schema_name,
35
- pg_class.relname as table_name,
36
- pg_index.indisprimary as is_primary_key,
37
- pg_index.indisunique as is_unique
38
- FROM pg_index
39
- JOIN pg_class ON pg_class.oid = pg_index.indrelid
40
- JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = ANY(pg_index.indkey)
41
- WHERE
42
- pg_attribute.attname = ${options.quote(this.getName())} AND
43
- pg_class.relname = ${options.quote(this.getTable().getName())}
44
- `)
45
-
46
- const indexes = []
47
-
48
- for (const indexRow of indexesRows) {
49
- const columnsIndex = new ColumnsIndex(this.getTable(), indexRow)
50
-
51
- indexes.push(columnsIndex)
52
- }
53
-
54
- return indexes
29
+ return indexes.filter((index) => index.getColumnNames().includes(this.getName()))
55
30
  }
56
31
 
57
32
  getDefault() {
@@ -1,6 +1,43 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseColumnsIndex from "../base-columns-index.js"
4
+ import TableIndex from "../../table-data/table-index.js"
5
+
6
+ /**
7
+ * PgsqlColumnsIndexDataType type.
8
+ * @typedef {object} PgsqlColumnsIndexDataType
9
+ * @property {string[]} columnNames - Ordered index column names.
10
+ * @property {string} index_name - Index name.
11
+ * @property {boolean} is_primary_key - Whether the index is primary.
12
+ * @property {boolean} is_unique - Whether the index is unique.
13
+ * @property {string} table_name - Table name.
14
+ */
4
15
 
5
16
  export default class VelociousDatabaseDriversPgsqlColumn extends BaseColumnsIndex {
17
+ /**
18
+ * Runs constructor.
19
+ * @param {import("../base-table.js").default} table - Table.
20
+ * @param {PgsqlColumnsIndexDataType} data - Grouped index metadata.
21
+ */
22
+ constructor(table, data) {
23
+ super(table, data)
24
+ this.indexData = data
25
+ }
26
+
27
+ /**
28
+ * Runs get column names.
29
+ * @returns {string[]} - Ordered index column names.
30
+ */
31
+ getColumnNames() { return this.indexData.columnNames }
32
+
33
+ /**
34
+ * Runs get table data index.
35
+ * @returns {TableIndex} - Table-data index.
36
+ */
37
+ getTableDataIndex() {
38
+ return new TableIndex(this.getColumnNames(), {
39
+ name: this.getName(),
40
+ unique: this.isUnique()
41
+ })
42
+ }
6
43
  }
@@ -4,6 +4,51 @@ import BaseTable from "../base-table.js"
4
4
  import Column from "./column.js"
5
5
  import ColumnsIndex from "./columns-index.js"
6
6
  import ForeignKey from "./foreign-key.js"
7
+ import { normalizeIndexMetadataRow } from "../index-metadata.js"
8
+
9
+ /**
10
+ * PgsqlGroupedIndexDataType type.
11
+ * @typedef {object} PgsqlGroupedIndexDataType
12
+ * @property {string[]} columnNames - Ordered index column names.
13
+ * @property {string} index_name - Index name.
14
+ * @property {boolean} is_primary_key - Whether the index is primary.
15
+ * @property {boolean} is_unique - Whether the index is unique.
16
+ * @property {string} table_name - Table name.
17
+ */
18
+
19
+ /**
20
+ * Groups ordered PostgreSQL index rows into one metadata value per index.
21
+ * @param {import("../index-metadata.js").IndexMetadataType[]} indexRows - Ordered index metadata rows.
22
+ * @returns {PgsqlGroupedIndexDataType[]} - Grouped index metadata.
23
+ */
24
+ export function groupPgsqlIndexRows(indexRows) {
25
+ /** @type {Map<string, PgsqlGroupedIndexDataType>} */
26
+ const indexDataByName = new Map()
27
+ /** @type {PgsqlGroupedIndexDataType[]} */
28
+ const groupedIndexData = []
29
+
30
+ for (const indexRow of indexRows) {
31
+ const existingIndexData = indexDataByName.get(indexRow.index_name)
32
+
33
+ if (existingIndexData) {
34
+ existingIndexData.columnNames.push(indexRow.column_name)
35
+ continue
36
+ }
37
+
38
+ const indexData = {
39
+ columnNames: [indexRow.column_name],
40
+ index_name: indexRow.index_name,
41
+ is_primary_key: indexRow.is_primary_key,
42
+ is_unique: indexRow.is_unique,
43
+ table_name: indexRow.table_name
44
+ }
45
+
46
+ indexDataByName.set(indexRow.index_name, indexData)
47
+ groupedIndexData.push(indexData)
48
+ }
49
+
50
+ return groupedIndexData
51
+ }
7
52
 
8
53
  export default class VelociousDatabaseDriversPgsqlTable extends BaseTable {
9
54
  /**
@@ -100,23 +145,29 @@ export default class VelociousDatabaseDriversPgsqlTable extends BaseTable {
100
145
 
101
146
  const indexesRows = await this.getDriver().query(`
102
147
  SELECT
103
- pg_attribute.attname AS column_name,
104
- pg_index.indexrelid::regclass as index_name,
105
- pg_class.relnamespace::regnamespace as schema_name,
106
- pg_class.relname as table_name,
107
- pg_index.indisprimary as is_primary_key,
108
- pg_index.indisunique as is_unique
148
+ index_attribute.attname AS column_name,
149
+ pg_index.indexrelid::regclass AS index_name,
150
+ pg_class.relname AS table_name,
151
+ pg_index.indisprimary AS is_primary_key,
152
+ pg_index.indisunique AS is_unique
109
153
  FROM pg_index
110
154
  JOIN pg_class ON pg_class.oid = pg_index.indrelid
111
- JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.attnum = ANY(pg_index.indkey)
155
+ JOIN LATERAL unnest(pg_index.indkey) WITH ORDINALITY AS index_columns(attribute_number, ordinal_position) ON true
156
+ JOIN pg_attribute AS index_attribute ON index_attribute.attrelid = pg_class.oid AND index_attribute.attnum = index_columns.attribute_number
112
157
  WHERE
113
- pg_class.relname = ${options.quote(this.getName())}
158
+ pg_class.relname = ${options.quote(this.getName())} AND
159
+ index_columns.ordinal_position <= pg_index.indnkeyatts
160
+ ORDER BY
161
+ pg_index.indexrelid,
162
+ index_columns.ordinal_position
114
163
  `)
115
164
 
116
165
  const indexes = []
117
166
 
118
- for (const indexRow of indexesRows) {
119
- const columnsIndex = new ColumnsIndex(this, indexRow)
167
+ const indexRows = indexesRows.map((indexRow) => normalizeIndexMetadataRow(indexRow))
168
+
169
+ for (const indexData of groupPgsqlIndexRows(indexRows)) {
170
+ const columnsIndex = new ColumnsIndex(this, indexData)
120
171
 
121
172
  indexes.push(columnsIndex)
122
173
  }