@adecore/database 0.0.1 → 0.16.0-beta.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 (400) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -1
  3. package/dist/DatabaseProvider.d.ts +13 -0
  4. package/dist/DatabaseProvider.js +17 -0
  5. package/dist/actions.d.ts +40 -0
  6. package/dist/actions.js +1 -0
  7. package/dist/client/client.d.ts +5 -0
  8. package/dist/client/client.js +245 -0
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +2 -0
  11. package/dist/client/types.d.ts +97 -0
  12. package/dist/client/types.js +13 -0
  13. package/dist/client-context.d.ts +16 -0
  14. package/dist/client-context.js +17 -0
  15. package/dist/connections/ConnectionForm.d.ts +10 -0
  16. package/dist/connections/ConnectionForm.js +145 -0
  17. package/dist/connections/ConnectionManager.d.ts +12 -0
  18. package/dist/connections/ConnectionManager.js +93 -0
  19. package/dist/connections/EngineIcon.d.ts +9 -0
  20. package/dist/connections/EngineIcon.js +7 -0
  21. package/dist/connections/connection-config.d.ts +16 -0
  22. package/dist/connections/connection-config.js +107 -0
  23. package/dist/connections/docker.d.ts +9 -0
  24. package/dist/connections/docker.js +38 -0
  25. package/dist/connections/engine-icons.d.ts +3 -0
  26. package/dist/connections/engine-icons.js +3 -0
  27. package/dist/connections/use-docker-containers.d.ts +17 -0
  28. package/dist/connections/use-docker-containers.js +26 -0
  29. package/dist/console/DestructiveDialog.d.ts +9 -0
  30. package/dist/console/DestructiveDialog.js +9 -0
  31. package/dist/console/HistoryPanel.d.ts +10 -0
  32. package/dist/console/HistoryPanel.js +15 -0
  33. package/dist/console/QueryConsole.d.ts +14 -0
  34. package/dist/console/QueryConsole.js +261 -0
  35. package/dist/console/SchemaPicker.d.ts +11 -0
  36. package/dist/console/SchemaPicker.js +8 -0
  37. package/dist/console/SqlEditor.d.ts +12 -0
  38. package/dist/console/SqlEditor.js +61 -0
  39. package/dist/console/StatementResultView.d.ts +18 -0
  40. package/dist/console/StatementResultView.js +32 -0
  41. package/dist/console/TransactionControls.d.ts +13 -0
  42. package/dist/console/TransactionControls.js +12 -0
  43. package/dist/console/destructive.d.ts +7 -0
  44. package/dist/console/destructive.js +43 -0
  45. package/dist/console/editing.d.ts +12 -0
  46. package/dist/console/editing.js +60 -0
  47. package/dist/console/history.d.ts +13 -0
  48. package/dist/console/history.js +43 -0
  49. package/dist/console/labels.d.ts +1 -0
  50. package/dist/console/labels.js +6 -0
  51. package/dist/console/outcome.d.ts +6 -0
  52. package/dist/console/outcome.js +11 -0
  53. package/dist/console/schemas.d.ts +2 -0
  54. package/dist/console/schemas.js +5 -0
  55. package/dist/console/shortcuts.d.ts +2 -0
  56. package/dist/console/shortcuts.js +3 -0
  57. package/dist/console/useConsoleHistory.d.ts +7 -0
  58. package/dist/console/useConsoleHistory.js +22 -0
  59. package/dist/ddl/alter-mysql.d.ts +4 -0
  60. package/dist/ddl/alter-mysql.js +79 -0
  61. package/dist/ddl/alter-sqlite.d.ts +4 -0
  62. package/dist/ddl/alter-sqlite.js +80 -0
  63. package/dist/ddl/alter.d.ts +4 -0
  64. package/dist/ddl/alter.js +24 -0
  65. package/dist/ddl/column.d.ts +9 -0
  66. package/dist/ddl/column.js +62 -0
  67. package/dist/ddl/create.d.ts +12 -0
  68. package/dist/ddl/create.js +74 -0
  69. package/dist/ddl/dialect.d.ts +12 -0
  70. package/dist/ddl/dialect.js +34 -0
  71. package/dist/ddl/diff.d.ts +34 -0
  72. package/dist/ddl/diff.js +111 -0
  73. package/dist/ddl/draft.d.ts +46 -0
  74. package/dist/ddl/draft.js +70 -0
  75. package/dist/ddl/index.d.ts +10 -0
  76. package/dist/ddl/index.js +10 -0
  77. package/dist/ddl/parse.d.ts +17 -0
  78. package/dist/ddl/parse.js +131 -0
  79. package/dist/ddl/statements.d.ts +6 -0
  80. package/dist/ddl/statements.js +12 -0
  81. package/dist/ddl/types.d.ts +6 -0
  82. package/dist/ddl/types.js +52 -0
  83. package/dist/ddl/validate.d.ts +7 -0
  84. package/dist/ddl/validate.js +38 -0
  85. package/dist/designer/ColumnsEditor.d.ts +2 -0
  86. package/dist/designer/ColumnsEditor.js +24 -0
  87. package/dist/designer/DesignerView.d.ts +25 -0
  88. package/dist/designer/DesignerView.js +33 -0
  89. package/dist/designer/ForeignKeysEditor.d.ts +7 -0
  90. package/dist/designer/ForeignKeysEditor.js +24 -0
  91. package/dist/designer/IndexesEditor.d.ts +2 -0
  92. package/dist/designer/IndexesEditor.js +13 -0
  93. package/dist/designer/NamePicker.d.ts +11 -0
  94. package/dist/designer/NamePicker.js +13 -0
  95. package/dist/designer/OptionsEditor.d.ts +2 -0
  96. package/dist/designer/OptionsEditor.js +17 -0
  97. package/dist/designer/SqlPreview.d.ts +12 -0
  98. package/dist/designer/SqlPreview.js +17 -0
  99. package/dist/designer/TableDesigner.d.ts +10 -0
  100. package/dist/designer/TableDesigner.js +103 -0
  101. package/dist/designer/edit.d.ts +17 -0
  102. package/dist/designer/edit.js +115 -0
  103. package/dist/designer/editor-props.d.ts +10 -0
  104. package/dist/designer/editor-props.js +1 -0
  105. package/dist/designer/plan.d.ts +9 -0
  106. package/dist/designer/plan.js +13 -0
  107. package/dist/designer/recover.d.ts +2 -0
  108. package/dist/designer/recover.js +18 -0
  109. package/dist/designer/useReferenceColumns.d.ts +3 -0
  110. package/dist/designer/useReferenceColumns.js +36 -0
  111. package/dist/explorer/DatabaseExplorer.d.ts +13 -0
  112. package/dist/explorer/DatabaseExplorer.js +264 -0
  113. package/dist/explorer/RowMenu.d.ts +11 -0
  114. package/dist/explorer/RowMenu.js +89 -0
  115. package/dist/explorer/TableDialog.d.ts +15 -0
  116. package/dist/explorer/TableDialog.js +50 -0
  117. package/dist/explorer/expansion.d.ts +7 -0
  118. package/dist/explorer/expansion.js +31 -0
  119. package/dist/explorer/table-statements.d.ts +5 -0
  120. package/dist/explorer/table-statements.js +12 -0
  121. package/dist/explorer/tree.d.ts +125 -0
  122. package/dist/explorer/tree.js +295 -0
  123. package/dist/explorer/use-explorer-loads.d.ts +12 -0
  124. package/dist/explorer/use-explorer-loads.js +109 -0
  125. package/dist/explorer/use-stored-expansion.d.ts +9 -0
  126. package/dist/explorer/use-stored-expansion.js +47 -0
  127. package/dist/grid/CellEditor.d.ts +9 -0
  128. package/dist/grid/CellEditor.js +29 -0
  129. package/dist/grid/CopyAsMenu.d.ts +7 -0
  130. package/dist/grid/CopyAsMenu.js +10 -0
  131. package/dist/grid/DataGrid.d.ts +36 -0
  132. package/dist/grid/DataGrid.js +512 -0
  133. package/dist/grid/EnumPicker.d.ts +17 -0
  134. package/dist/grid/EnumPicker.js +53 -0
  135. package/dist/grid/GridHeaderCell.d.ts +36 -0
  136. package/dist/grid/GridHeaderCell.js +69 -0
  137. package/dist/grid/RecordView.d.ts +20 -0
  138. package/dist/grid/RecordView.js +126 -0
  139. package/dist/grid/ValueDock.d.ts +11 -0
  140. package/dist/grid/ValueDock.js +28 -0
  141. package/dist/grid/aggregates.d.ts +22 -0
  142. package/dist/grid/aggregates.js +62 -0
  143. package/dist/grid/cell-range.d.ts +14 -0
  144. package/dist/grid/cell-range.js +11 -0
  145. package/dist/grid/column-layout.d.ts +18 -0
  146. package/dist/grid/column-layout.js +49 -0
  147. package/dist/grid/column-selection.d.ts +11 -0
  148. package/dist/grid/column-selection.js +28 -0
  149. package/dist/grid/column-view.d.ts +10 -0
  150. package/dist/grid/column-view.js +23 -0
  151. package/dist/grid/copy-formats.d.ts +15 -0
  152. package/dist/grid/copy-formats.js +80 -0
  153. package/dist/grid/display.d.ts +20 -0
  154. package/dist/grid/display.js +81 -0
  155. package/dist/grid/edit-value.d.ts +5 -0
  156. package/dist/grid/edit-value.js +55 -0
  157. package/dist/grid/enum-type.d.ts +7 -0
  158. package/dist/grid/enum-type.js +52 -0
  159. package/dist/grid/focused-value.d.ts +4 -0
  160. package/dist/grid/focused-value.js +19 -0
  161. package/dist/grid/layout.d.ts +22 -0
  162. package/dist/grid/layout.js +74 -0
  163. package/dist/grid/navigation.d.ts +15 -0
  164. package/dist/grid/navigation.js +52 -0
  165. package/dist/grid/row-selection.d.ts +9 -0
  166. package/dist/grid/row-selection.js +24 -0
  167. package/dist/grid/sort.d.ts +7 -0
  168. package/dist/grid/sort.js +7 -0
  169. package/dist/grid/types.d.ts +30 -0
  170. package/dist/grid/types.js +9 -0
  171. package/dist/host/helper-path.d.ts +7 -0
  172. package/dist/host/helper-path.js +15 -0
  173. package/dist/host/host.d.ts +2 -0
  174. package/dist/host/host.js +343 -0
  175. package/dist/host/index.d.ts +5 -0
  176. package/dist/host/index.js +4 -0
  177. package/dist/host/spawn.d.ts +7 -0
  178. package/dist/host/spawn.js +98 -0
  179. package/dist/host/types.d.ts +19 -0
  180. package/dist/host/types.js +1 -0
  181. package/dist/host/validate.d.ts +10 -0
  182. package/dist/host/validate.js +339 -0
  183. package/dist/index.d.ts +14 -0
  184. package/dist/index.js +13 -0
  185. package/dist/locales/en.json +577 -0
  186. package/dist/locales/nl.json +577 -0
  187. package/dist/locales.d.ts +4 -0
  188. package/dist/locales.js +14 -0
  189. package/dist/protocol/connection.d.ts +56 -0
  190. package/dist/protocol/connection.js +1 -0
  191. package/dist/protocol/errors.d.ts +7 -0
  192. package/dist/protocol/errors.js +1 -0
  193. package/dist/protocol/index.d.ts +5 -0
  194. package/dist/protocol/index.js +3 -0
  195. package/dist/protocol/messages.d.ts +250 -0
  196. package/dist/protocol/messages.js +2 -0
  197. package/dist/protocol/schema.d.ts +53 -0
  198. package/dist/protocol/schema.js +1 -0
  199. package/dist/protocol/values.d.ts +21 -0
  200. package/dist/protocol/values.js +15 -0
  201. package/dist/sql-split.d.ts +15 -0
  202. package/dist/sql-split.js +201 -0
  203. package/dist/sql.d.ts +29 -0
  204. package/dist/sql.js +104 -0
  205. package/dist/structure/StructureView.d.ts +14 -0
  206. package/dist/structure/StructureView.js +99 -0
  207. package/dist/structure/structure-text.d.ts +2 -0
  208. package/dist/structure/structure-text.js +2 -0
  209. package/dist/table/CommandField.d.ts +14 -0
  210. package/dist/table/CommandField.js +157 -0
  211. package/dist/table/ImportDialog.d.ts +27 -0
  212. package/dist/table/ImportDialog.js +28 -0
  213. package/dist/table/TableStatusBar.d.ts +19 -0
  214. package/dist/table/TableStatusBar.js +22 -0
  215. package/dist/table/TableToolbar.d.ts +43 -0
  216. package/dist/table/TableToolbar.js +16 -0
  217. package/dist/table/TableView.d.ts +13 -0
  218. package/dist/table/TableView.js +508 -0
  219. package/dist/table/command-field.d.ts +57 -0
  220. package/dist/table/command-field.js +203 -0
  221. package/dist/table/grid-rows.d.ts +23 -0
  222. package/dist/table/grid-rows.js +55 -0
  223. package/dist/table/import-mapping.d.ts +7 -0
  224. package/dist/table/import-mapping.js +28 -0
  225. package/dist/table/layout-store.d.ts +13 -0
  226. package/dist/table/layout-store.js +43 -0
  227. package/dist/table/paging.d.ts +12 -0
  228. package/dist/table/paging.js +20 -0
  229. package/dist/table/pending.d.ts +32 -0
  230. package/dist/table/pending.js +94 -0
  231. package/dist/table/references.d.ts +12 -0
  232. package/dist/table/references.js +21 -0
  233. package/dist/table/useLoaded.d.ts +7 -0
  234. package/dist/table/useLoaded.js +36 -0
  235. package/dist/table/useStoredLayout.d.ts +8 -0
  236. package/dist/table/useStoredLayout.js +39 -0
  237. package/dist/table/useTableTransfer.d.ts +51 -0
  238. package/dist/table/useTableTransfer.js +125 -0
  239. package/dist/testing/fake.d.ts +21 -0
  240. package/dist/testing/fake.js +372 -0
  241. package/dist/testing/index.d.ts +1 -0
  242. package/dist/testing/index.js +1 -0
  243. package/dist/use-popup-press.d.ts +1 -0
  244. package/dist/use-popup-press.js +24 -0
  245. package/dist/use-schema-change.d.ts +1 -0
  246. package/dist/use-schema-change.js +13 -0
  247. package/dist/use-stable-callback.d.ts +1 -0
  248. package/dist/use-stable-callback.js +9 -0
  249. package/dist/value/ValuePanel.d.ts +14 -0
  250. package/dist/value/ValuePanel.js +103 -0
  251. package/dist/value/draft.d.ts +33 -0
  252. package/dist/value/draft.js +44 -0
  253. package/dist/value/hex.d.ts +14 -0
  254. package/dist/value/hex.js +46 -0
  255. package/dist/value/json.d.ts +9 -0
  256. package/dist/value/json.js +71 -0
  257. package/dist/value/views.d.ts +22 -0
  258. package/dist/value/views.js +54 -0
  259. package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
  260. package/dist/workbench/DatabaseWorkbench.js +179 -0
  261. package/dist/workbench/DesignerTab.d.ts +10 -0
  262. package/dist/workbench/DesignerTab.js +22 -0
  263. package/dist/workbench/TableTab.d.ts +10 -0
  264. package/dist/workbench/TableTab.js +8 -0
  265. package/dist/workbench/tabs.d.ts +42 -0
  266. package/dist/workbench/tabs.js +139 -0
  267. package/package.json +79 -3
  268. package/src/DatabaseProvider.tsx +37 -0
  269. package/src/actions.ts +34 -0
  270. package/src/client/client.ts +324 -0
  271. package/src/client/index.ts +18 -0
  272. package/src/client/types.ts +161 -0
  273. package/src/client-context.ts +35 -0
  274. package/src/connections/ConnectionForm.tsx +474 -0
  275. package/src/connections/ConnectionManager.tsx +224 -0
  276. package/src/connections/EngineIcon.tsx +18 -0
  277. package/src/connections/connection-config.ts +143 -0
  278. package/src/connections/docker.ts +50 -0
  279. package/src/connections/engine-icons.ts +5 -0
  280. package/src/connections/use-docker-containers.ts +41 -0
  281. package/src/console/DestructiveDialog.tsx +40 -0
  282. package/src/console/HistoryPanel.tsx +67 -0
  283. package/src/console/QueryConsole.tsx +439 -0
  284. package/src/console/SchemaPicker.tsx +32 -0
  285. package/src/console/SqlEditor.tsx +95 -0
  286. package/src/console/StatementResultView.tsx +176 -0
  287. package/src/console/TransactionControls.tsx +53 -0
  288. package/src/console/destructive.ts +60 -0
  289. package/src/console/editing.ts +77 -0
  290. package/src/console/history.ts +62 -0
  291. package/src/console/labels.ts +7 -0
  292. package/src/console/outcome.ts +14 -0
  293. package/src/console/schemas.ts +7 -0
  294. package/src/console/shortcuts.ts +4 -0
  295. package/src/console/useConsoleHistory.ts +39 -0
  296. package/src/ddl/alter-mysql.ts +90 -0
  297. package/src/ddl/alter-sqlite.ts +93 -0
  298. package/src/ddl/alter.ts +29 -0
  299. package/src/ddl/column.ts +74 -0
  300. package/src/ddl/create.ts +92 -0
  301. package/src/ddl/dialect.ts +53 -0
  302. package/src/ddl/diff.ts +164 -0
  303. package/src/ddl/draft.ts +136 -0
  304. package/src/ddl/index.ts +10 -0
  305. package/src/ddl/parse.ts +161 -0
  306. package/src/ddl/statements.ts +20 -0
  307. package/src/ddl/types.ts +60 -0
  308. package/src/ddl/validate.ts +47 -0
  309. package/src/designer/ColumnsEditor.tsx +204 -0
  310. package/src/designer/DesignerView.tsx +184 -0
  311. package/src/designer/ForeignKeysEditor.tsx +132 -0
  312. package/src/designer/IndexesEditor.tsx +63 -0
  313. package/src/designer/NamePicker.tsx +44 -0
  314. package/src/designer/OptionsEditor.tsx +101 -0
  315. package/src/designer/SqlPreview.tsx +56 -0
  316. package/src/designer/TableDesigner.tsx +186 -0
  317. package/src/designer/edit.ts +137 -0
  318. package/src/designer/editor-props.ts +13 -0
  319. package/src/designer/plan.ts +25 -0
  320. package/src/designer/recover.ts +20 -0
  321. package/src/designer/useReferenceColumns.ts +54 -0
  322. package/src/explorer/DatabaseExplorer.tsx +450 -0
  323. package/src/explorer/RowMenu.tsx +134 -0
  324. package/src/explorer/TableDialog.tsx +80 -0
  325. package/src/explorer/expansion.ts +47 -0
  326. package/src/explorer/table-statements.ts +18 -0
  327. package/src/explorer/tree.ts +453 -0
  328. package/src/explorer/use-explorer-loads.ts +157 -0
  329. package/src/explorer/use-stored-expansion.ts +66 -0
  330. package/src/grid/CellEditor.tsx +53 -0
  331. package/src/grid/CopyAsMenu.tsx +33 -0
  332. package/src/grid/DataGrid.tsx +871 -0
  333. package/src/grid/EnumPicker.tsx +108 -0
  334. package/src/grid/GridHeaderCell.tsx +243 -0
  335. package/src/grid/RecordView.tsx +293 -0
  336. package/src/grid/ValueDock.tsx +52 -0
  337. package/src/grid/aggregates.ts +94 -0
  338. package/src/grid/cell-range.ts +26 -0
  339. package/src/grid/column-layout.ts +71 -0
  340. package/src/grid/column-selection.ts +42 -0
  341. package/src/grid/column-view.ts +35 -0
  342. package/src/grid/copy-formats.ts +108 -0
  343. package/src/grid/display.ts +109 -0
  344. package/src/grid/edit-value.ts +60 -0
  345. package/src/grid/enum-type.ts +62 -0
  346. package/src/grid/focused-value.ts +23 -0
  347. package/src/grid/layout.ts +91 -0
  348. package/src/grid/navigation.ts +74 -0
  349. package/src/grid/row-selection.ts +35 -0
  350. package/src/grid/sort.ts +12 -0
  351. package/src/grid/types.ts +59 -0
  352. package/src/host/helper-path.ts +41 -0
  353. package/src/host/host.ts +456 -0
  354. package/src/host/index.ts +5 -0
  355. package/src/host/spawn.ts +124 -0
  356. package/src/host/types.ts +46 -0
  357. package/src/host/validate.ts +402 -0
  358. package/src/index.ts +37 -0
  359. package/src/locales/en.json +577 -0
  360. package/src/locales/nl.json +577 -0
  361. package/src/locales.ts +18 -0
  362. package/src/protocol/connection.ts +86 -0
  363. package/src/protocol/errors.ts +38 -0
  364. package/src/protocol/index.ts +33 -0
  365. package/src/protocol/messages.ts +168 -0
  366. package/src/protocol/schema.ts +76 -0
  367. package/src/protocol/values.ts +52 -0
  368. package/src/sql-split.ts +220 -0
  369. package/src/sql.ts +152 -0
  370. package/src/structure/StructureView.tsx +269 -0
  371. package/src/structure/structure-text.ts +4 -0
  372. package/src/table/CommandField.tsx +318 -0
  373. package/src/table/ImportDialog.tsx +168 -0
  374. package/src/table/TableStatusBar.tsx +91 -0
  375. package/src/table/TableToolbar.tsx +217 -0
  376. package/src/table/TableView.tsx +880 -0
  377. package/src/table/command-field.ts +285 -0
  378. package/src/table/grid-rows.ts +80 -0
  379. package/src/table/import-mapping.ts +37 -0
  380. package/src/table/layout-store.ts +62 -0
  381. package/src/table/paging.ts +33 -0
  382. package/src/table/pending.ts +143 -0
  383. package/src/table/references.ts +38 -0
  384. package/src/table/useLoaded.ts +58 -0
  385. package/src/table/useStoredLayout.ts +53 -0
  386. package/src/table/useTableTransfer.ts +187 -0
  387. package/src/testing/fake.ts +533 -0
  388. package/src/testing/index.ts +1 -0
  389. package/src/use-popup-press.ts +27 -0
  390. package/src/use-schema-change.ts +19 -0
  391. package/src/use-stable-callback.ts +12 -0
  392. package/src/value/ValuePanel.tsx +254 -0
  393. package/src/value/draft.ts +72 -0
  394. package/src/value/hex.ts +62 -0
  395. package/src/value/json.ts +74 -0
  396. package/src/value/views.ts +68 -0
  397. package/src/workbench/DatabaseWorkbench.tsx +348 -0
  398. package/src/workbench/DesignerTab.tsx +37 -0
  399. package/src/workbench/TableTab.tsx +27 -0
  400. package/src/workbench/tabs.ts +184 -0
@@ -0,0 +1,52 @@
1
+ import { useRef, useState, type ReactNode, type Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { ColumnResizeHandle, useColumnResize } from '@adecore/ui';
4
+
5
+ export const VALUE_PANEL_WIDTH = 320;
6
+ export const VALUE_PANEL_MIN_WIDTH = 240;
7
+
8
+ export interface ValueDockProps {
9
+ open: boolean;
10
+ /* Drawn on the right of the children while the dock is open. */
11
+ panel: ReactNode;
12
+ /* The grid and whatever shares its column. */
13
+ children: ReactNode;
14
+ className?: string;
15
+ ref?: Ref<HTMLDivElement>;
16
+ }
17
+
18
+ /* A grid with a panel along its right edge that a person can widen by its left edge. */
19
+ export function ValueDock({ open, panel, children, className, ref }: ValueDockProps) {
20
+ const root = useRef<HTMLDivElement | null>(null);
21
+ const dock = useRef<HTMLDivElement>(null);
22
+ const [width, setWidth] = useState(VALUE_PANEL_WIDTH);
23
+ const { startResize } = useColumnResize(dock, {
24
+ size: width,
25
+ min: VALUE_PANEL_MIN_WIDTH,
26
+ from: 'right',
27
+ max: () => (root.current?.clientWidth ?? 960) - VALUE_PANEL_MIN_WIDTH,
28
+ onSize: setWidth
29
+ });
30
+
31
+ return (
32
+ <div
33
+ ref={(node) => {
34
+ root.current = node;
35
+ if (typeof ref === 'function') {
36
+ ref(node);
37
+ } else if (ref) {
38
+ ref.current = node;
39
+ }
40
+ }}
41
+ className={clsx('flex min-h-0 min-w-0 flex-1', className)}
42
+ >
43
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col">{children}</div>
44
+ {open && (
45
+ <div ref={dock} className="relative flex min-h-0 shrink-0 border-l border-border" style={{ width }}>
46
+ <ColumnResizeHandle from="right" onPointerDown={startResize} className="hover:bg-border-strong" />
47
+ {panel}
48
+ </div>
49
+ )}
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,94 @@
1
+ import type { ValueKind } from '../protocol/index.ts';
2
+ import type { Shown } from './display.ts';
3
+
4
+ const NUMERIC_KINDS: ReadonlySet<ValueKind> = new Set(['integer', 'decimal', 'float']);
5
+ const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
6
+
7
+ /* A block of rows, and the columns (indexes into the columns of the grid) it spans. */
8
+ export interface RangeBlock {
9
+ readonly top: number;
10
+ readonly bottom: number;
11
+ readonly columns: readonly number[];
12
+ }
13
+
14
+ export interface NumericAggregates {
15
+ /* The cells that held a number; NULLs and the cells of other columns do not count. */
16
+ readonly count: number;
17
+ readonly sum: number;
18
+ readonly average: number;
19
+ readonly minimum: number;
20
+ readonly maximum: number;
21
+ /* The extremes as the cells spell them, which a decimal or a big integer keeps in full. */
22
+ readonly minimumText: string;
23
+ readonly maximumText: string;
24
+ }
25
+
26
+ export interface Aggregates {
27
+ /* Every cell of the block. */
28
+ readonly count: number;
29
+ /* `null` when no cell of the block held a number. */
30
+ readonly numeric: NumericAggregates | null;
31
+ }
32
+
33
+ const numberOf = (cell: Shown): { value: number; text: string } | null => {
34
+ if (typeof cell === 'number') {
35
+ return Number.isFinite(cell) ? { value: cell, text: String(cell) } : null;
36
+ }
37
+ if (typeof cell === 'string') {
38
+ const text = cell.trim();
39
+ return NUMERIC_TEXT.test(text) ? { value: Number(text), text } : null;
40
+ }
41
+ return null;
42
+ };
43
+
44
+ /* The size of a block in cells, which a footer shows aggregates for once it passes one. */
45
+ export const blockSize = (block: RangeBlock): number => Math.max(0, block.bottom - block.top + 1) * block.columns.length;
46
+
47
+ /*
48
+ * The count of a block, and for the cells in numeric columns their sum, average, minimum and maximum.
49
+ * Only the rows the grid has are counted, which is the loaded page.
50
+ */
51
+ export const aggregateBlock = (rows: readonly (readonly Shown[])[], kinds: readonly ValueKind[], block: RangeBlock): Aggregates => {
52
+ let count = 0;
53
+ let numbers = 0;
54
+ let sum = 0;
55
+ let minimum: { value: number; text: string } | null = null;
56
+ let maximum: { value: number; text: string } | null = null;
57
+ for (let row = block.top; row <= block.bottom; row++) {
58
+ const cells = rows[row];
59
+ if (cells === undefined) {
60
+ continue;
61
+ }
62
+ for (const column of block.columns) {
63
+ count++;
64
+ const kind = kinds[column];
65
+ const found = kind !== undefined && NUMERIC_KINDS.has(kind) ? numberOf(cells[column] ?? null) : null;
66
+ if (found === null) {
67
+ continue;
68
+ }
69
+ numbers++;
70
+ sum += found.value;
71
+ if (minimum === null || found.value < minimum.value) {
72
+ minimum = found;
73
+ }
74
+ if (maximum === null || found.value > maximum.value) {
75
+ maximum = found;
76
+ }
77
+ }
78
+ }
79
+ if (numbers === 0 || minimum === null || maximum === null) {
80
+ return { count, numeric: null };
81
+ }
82
+ return {
83
+ count,
84
+ numeric: {
85
+ count: numbers,
86
+ sum,
87
+ average: sum / numbers,
88
+ minimum: minimum.value,
89
+ maximum: maximum.value,
90
+ minimumText: minimum.text,
91
+ maximumText: maximum.text
92
+ }
93
+ };
94
+ };
@@ -0,0 +1,26 @@
1
+ import type { CellPosition } from './navigation.ts';
2
+
3
+ /* A block of cells, both ends included. Columns are positions as drawn, so a block stays rectangular on screen. */
4
+ export interface CellRect {
5
+ readonly top: number;
6
+ readonly bottom: number;
7
+ readonly left: number;
8
+ readonly right: number;
9
+ }
10
+
11
+ /* The block between two cells, whichever corner each one is. */
12
+ export const rectBetween = (from: CellPosition, to: CellPosition): CellRect => ({
13
+ top: Math.min(from.row, to.row),
14
+ bottom: Math.max(from.row, to.row),
15
+ left: Math.min(from.column, to.column),
16
+ right: Math.max(from.column, to.column)
17
+ });
18
+
19
+ export const rectContains = (rect: CellRect | null, row: number, column: number): boolean =>
20
+ rect !== null && row >= rect.top && row <= rect.bottom && column >= rect.left && column <= rect.right;
21
+
22
+ /* Every cell of a grid, or `null` when it has none. */
23
+ export const fullRect = (rows: number, columns: number): CellRect | null =>
24
+ rows === 0 || columns === 0 ? null : { top: 0, bottom: rows - 1, left: 0, right: columns - 1 };
25
+
26
+ export const rectSize = (rect: CellRect): { rows: number; columns: number } => ({ rows: rect.bottom - rect.top + 1, columns: rect.right - rect.left + 1 });
@@ -0,0 +1,71 @@
1
+ import type { ColumnView } from './column-view.ts';
2
+ import { MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.ts';
3
+
4
+ /* What a person did to the columns of a grid, by column name so it outlives a change of the column order. */
5
+ export interface GridLayout {
6
+ /* Only the widths a person set by dragging or fitting. */
7
+ readonly widths: Readonly<Record<string, number>>;
8
+ readonly hidden: readonly string[];
9
+ /* In the order they were pinned. */
10
+ readonly pinned: readonly string[];
11
+ }
12
+
13
+ export const NO_GRID_LAYOUT: GridLayout = { widths: {}, hidden: [], pinned: [] };
14
+
15
+ export interface ColumnState {
16
+ readonly resized: Readonly<Record<number, number>>;
17
+ readonly view: ColumnView;
18
+ }
19
+
20
+ /* The state of a grid for columns it has, from a layout. Names it does not have are skipped; a layout that would hide every column hides none. */
21
+ export const restoreLayout = (columns: readonly { readonly name: string }[], layout: GridLayout | undefined): ColumnState => {
22
+ const resized: Record<number, number> = {};
23
+ const hidden = new Set<number>();
24
+ const pinned: number[] = [];
25
+ if (layout !== undefined) {
26
+ columns.forEach((column, index) => {
27
+ const width = layout.widths[column.name];
28
+ if (width !== undefined) {
29
+ resized[index] = Math.max(MIN_COLUMN_WIDTH, Math.min(Math.round(width), MAX_RESIZED_WIDTH));
30
+ }
31
+ if (layout.hidden.includes(column.name)) {
32
+ hidden.add(index);
33
+ }
34
+ });
35
+ for (const name of layout.pinned) {
36
+ const index = columns.findIndex((column) => column.name === name);
37
+ if (index >= 0 && !hidden.has(index) && !pinned.includes(index)) {
38
+ pinned.push(index);
39
+ }
40
+ }
41
+ }
42
+ return { resized, view: { hidden: hidden.size >= columns.length ? new Set() : hidden, pinned } };
43
+ };
44
+
45
+ /* The layout of a grid's state, with the columns by name. */
46
+ export const captureLayout = (columns: readonly { readonly name: string }[], state: ColumnState): GridLayout => {
47
+ const widths: Record<string, number> = {};
48
+ for (const [index, width] of Object.entries(state.resized)) {
49
+ const column = columns[Number(index)];
50
+ if (column !== undefined) {
51
+ widths[column.name] = width;
52
+ }
53
+ }
54
+ return {
55
+ widths,
56
+ hidden: [...state.view.hidden].sort((a, b) => a - b).flatMap((index) => (columns[index] === undefined ? [] : [columns[index]!.name])),
57
+ pinned: state.view.pinned.flatMap((index) => (columns[index] === undefined ? [] : [columns[index]!.name]))
58
+ };
59
+ };
60
+
61
+ const sameList = (a: readonly string[], b: readonly string[]): boolean => a.length === b.length && a.every((item, index) => item === b[index]);
62
+
63
+ export const sameLayout = (a: GridLayout, b: GridLayout): boolean => {
64
+ const names = Object.keys(a.widths);
65
+ return (
66
+ names.length === Object.keys(b.widths).length &&
67
+ names.every((name) => a.widths[name] === b.widths[name]) &&
68
+ sameList(a.hidden, b.hidden) &&
69
+ sameList(a.pinned, b.pinned)
70
+ );
71
+ };
@@ -0,0 +1,42 @@
1
+ /* The columns picked by a click on their headers, by index in the columns the grid was given. */
2
+ export interface ColumnSelection {
3
+ readonly columns: ReadonlySet<number>;
4
+ /* The column a Shift-click extends from; `null` when nothing is picked. */
5
+ readonly anchor: number | null;
6
+ }
7
+
8
+ export interface ColumnClick {
9
+ readonly shiftKey: boolean;
10
+ readonly mod: boolean;
11
+ }
12
+
13
+ export const NO_COLUMN_SELECTION: ColumnSelection = { columns: new Set(), anchor: null };
14
+
15
+ /*
16
+ * The selection after a click on the header of `index`. A plain click picks that column alone, Mod
17
+ * toggles it, and Shift picks every column drawn between the anchor and it (added to the others with Mod).
18
+ * `order` is the columns as drawn, so a range follows what the person sees.
19
+ */
20
+ export const selectColumn = (current: ColumnSelection, order: readonly number[], index: number, click: ColumnClick): ColumnSelection => {
21
+ const at = order.indexOf(index);
22
+ if (at < 0) {
23
+ return current;
24
+ }
25
+ const anchorAt = current.anchor === null ? -1 : order.indexOf(current.anchor);
26
+ if (click.shiftKey && anchorAt >= 0) {
27
+ const range = order.slice(Math.min(anchorAt, at), Math.max(anchorAt, at) + 1);
28
+ return { columns: new Set(click.mod ? [...current.columns, ...range] : range), anchor: current.anchor };
29
+ }
30
+ if (click.mod) {
31
+ const next = new Set(current.columns);
32
+ if (next.delete(index)) {
33
+ return { columns: next, anchor: next.size === 0 ? null : current.anchor };
34
+ }
35
+ next.add(index);
36
+ return { columns: next, anchor: index };
37
+ }
38
+ return { columns: new Set([index]), anchor: index };
39
+ };
40
+
41
+ /* The picked columns that are drawn, in the order they are drawn. */
42
+ export const shownSelection = (current: ColumnSelection, order: readonly number[]): number[] => order.filter((index) => current.columns.has(index));
@@ -0,0 +1,35 @@
1
+ /* Which columns of a grid are hidden and which are pinned, by their index in the columns the grid was given. */
2
+ export interface ColumnView {
3
+ readonly hidden: ReadonlySet<number>;
4
+ /* In the order they were pinned, which is the order they sit in. */
5
+ readonly pinned: readonly number[];
6
+ }
7
+
8
+ export const NO_COLUMN_VIEW: ColumnView = { hidden: new Set(), pinned: [] };
9
+
10
+ /* The columns to draw as indexes: the pinned ones first in pin order, then the rest as they come, hidden ones left out. */
11
+ export const displayOrder = (count: number, view: ColumnView): number[] => {
12
+ const pinned = view.pinned.filter((index) => index < count && !view.hidden.has(index));
13
+ const rest: number[] = [];
14
+ for (let i = 0; i < count; i++) {
15
+ if (!view.hidden.has(i) && !pinned.includes(i)) {
16
+ rest.push(i);
17
+ }
18
+ }
19
+ return [...pinned, ...rest];
20
+ };
21
+
22
+ export const hideColumn = (view: ColumnView, index: number): ColumnView => ({
23
+ hidden: new Set([...view.hidden, index]),
24
+ pinned: view.pinned.filter((pinned) => pinned !== index)
25
+ });
26
+
27
+ export const showAllColumns = (view: ColumnView): ColumnView => ({ ...view, hidden: new Set() });
28
+
29
+ export const togglePin = (view: ColumnView, index: number): ColumnView => ({
30
+ ...view,
31
+ pinned: view.pinned.includes(index) ? view.pinned.filter((pinned) => pinned !== index) : [...view.pinned, index]
32
+ });
33
+
34
+ /* How many of the first columns of `displayOrder` are pinned. */
35
+ export const pinnedCount = (count: number, view: ColumnView): number => view.pinned.filter((index) => index < count && !view.hidden.has(index)).length;
@@ -0,0 +1,108 @@
1
+ import type { ValueKind } from '../protocol/index.ts';
2
+ import { qualifiedName, quoteIdentifier, sqlLiteral, type SqlTarget } from '../sql.ts';
3
+ import { copyTextOf, type Shown } from './display.ts';
4
+
5
+ export type CopyFormat = 'tsv' | 'csv' | 'json' | 'sql';
6
+
7
+ export const COPY_FORMATS: readonly CopyFormat[] = ['tsv', 'csv', 'json', 'sql'];
8
+
9
+ export interface CopyColumn {
10
+ readonly name: string;
11
+ readonly kind: ValueKind;
12
+ }
13
+
14
+ export interface CopyInput {
15
+ readonly columns: readonly CopyColumn[];
16
+ /* One array per row, one cell per column. A preview copies as the preview. */
17
+ readonly rows: readonly (readonly Shown[])[];
18
+ /* Needed for `sql`. */
19
+ readonly target?: SqlTarget;
20
+ }
21
+
22
+ const NUMERIC_KINDS: ReadonlySet<ValueKind> = new Set(['integer', 'decimal', 'float']);
23
+ const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
24
+
25
+ const quoteField = (text: string): string => `"${text.replaceAll('"', '""')}"`;
26
+
27
+ /* Tabs and line breaks would split a cell on paste, so a cell that holds one is quoted the way a spreadsheet does. */
28
+ const tsvField = (text: string): string => (/[\t\r\n"]/.test(text) ? quoteField(text) : text);
29
+
30
+ const csvField = (text: string): string => (/[",\r\n]/.test(text) ? quoteField(text) : text);
31
+
32
+ const csvText = (cell: Shown): string => (cell === null ? '' : copyTextOf(cell));
33
+
34
+ const jsonOf = (cell: Shown, kind: ValueKind): string => {
35
+ if (cell === null) {
36
+ return 'null';
37
+ }
38
+ if (typeof cell === 'boolean') {
39
+ return String(cell);
40
+ }
41
+ if (typeof cell === 'number') {
42
+ if (kind === 'boolean' && (cell === 0 || cell === 1)) {
43
+ return String(cell === 1);
44
+ }
45
+ return Number.isFinite(cell) ? String(cell) : 'null';
46
+ }
47
+ if (typeof cell === 'string') {
48
+ if (NUMERIC_KINDS.has(kind) && JSON_NUMBER.test(cell)) {
49
+ return cell;
50
+ }
51
+ if (kind === 'json') {
52
+ try {
53
+ return JSON.stringify(JSON.parse(cell));
54
+ } catch {
55
+ return JSON.stringify(cell);
56
+ }
57
+ }
58
+ return JSON.stringify(cell);
59
+ }
60
+ return cell.kind === 'default' ? 'null' : JSON.stringify(copyTextOf(cell));
61
+ };
62
+
63
+ const toTsv = ({ rows }: CopyInput): string => rows.map((row) => row.map((cell) => tsvField(copyTextOf(cell))).join('\t')).join('\n');
64
+
65
+ const toCsv = ({ columns, rows }: CopyInput): string =>
66
+ [columns.map((column) => csvField(column.name)), ...rows.map((row) => row.map((cell) => csvField(csvText(cell))))]
67
+ .map((line) => line.join(','))
68
+ .join('\r\n');
69
+
70
+ const toJson = ({ columns, rows }: CopyInput): string => {
71
+ if (rows.length === 0) {
72
+ return '[]';
73
+ }
74
+ const objects = rows.map((row) => {
75
+ if (columns.length === 0) {
76
+ return ' {}';
77
+ }
78
+ const fields = columns.map((column, index) => ` ${JSON.stringify(column.name)}: ${jsonOf(row[index] ?? null, column.kind)}`);
79
+ return ` {\n${fields.join(',\n')}\n }`;
80
+ });
81
+ return `[\n${objects.join(',\n')}\n]`;
82
+ };
83
+
84
+ const toSql = ({ columns, rows, target }: CopyInput): string => {
85
+ if (target === undefined) {
86
+ throw new Error('Copying as SQL needs the table the rows belong to.');
87
+ }
88
+ const head = `INSERT INTO ${qualifiedName(target)} (${columns.map((column) => quoteIdentifier(target.engine, column.name)).join(', ')}) VALUES`;
89
+ return rows.map((row) => `${head} (${columns.map((column, index) => sqlLiteral(target.engine, row[index] ?? null, column.kind)).join(', ')});`).join('\n');
90
+ };
91
+
92
+ /*
93
+ * A block of cells as text. TSV is what a spreadsheet pastes and has no header; CSV follows RFC 4180
94
+ * with a header and leaves NULL empty; JSON is an array of objects that keeps numbers, booleans and
95
+ * NULL as what they are; SQL is one INSERT per row.
96
+ */
97
+ export const formatCopy = (format: CopyFormat, input: CopyInput): string => {
98
+ switch (format) {
99
+ case 'tsv':
100
+ return toTsv(input);
101
+ case 'csv':
102
+ return toCsv(input);
103
+ case 'json':
104
+ return toJson(input);
105
+ case 'sql':
106
+ return toSql(input);
107
+ }
108
+ };
@@ -0,0 +1,109 @@
1
+ import { formatBytes, formatNumeral } from '@adecore/ui/format';
2
+ import type { BinaryValue, Cell, EditValue, ValueKind } from '../protocol/index.ts';
3
+
4
+ /* What a cell shows: a cell of a result, or the column's default for a value an edit set to `DEFAULT`. */
5
+ export type Shown = Cell | { readonly kind: 'default' };
6
+
7
+ export type CellTone = 'value' | 'null' | 'default';
8
+
9
+ export interface CellView {
10
+ readonly text: string;
11
+ readonly tone: CellTone;
12
+ readonly align: 'start' | 'end';
13
+ /* Faint text after the value, such as the size of a binary value. */
14
+ readonly suffix?: string;
15
+ }
16
+
17
+ /* How a numeric cell is drawn: as the server wrote it, or in the notation of the person's region. */
18
+ export type NumberNotation = 'database' | 'region';
19
+
20
+ /* A cell never draws more than this, however long the text: the DOM stays small and the cell is one line anyway. */
21
+ export const DISPLAY_LIMIT = 200;
22
+ /* How many bytes of a binary value the cell spells out. */
23
+ export const BINARY_PREVIEW_BYTES = 8;
24
+
25
+ const NUMERIC_KINDS: ReadonlySet<ValueKind> = new Set(['integer', 'decimal', 'float']);
26
+
27
+ /* Whether the cells of a column of this kind sit against the end of their cell, as numbers do. */
28
+ export const isNumericKind = (kind: ValueKind): boolean => NUMERIC_KINDS.has(kind);
29
+
30
+ const ELLIPSIS = '…';
31
+
32
+ const isObject = (cell: Shown): cell is Exclude<Shown, null | boolean | number | string> => cell !== null && typeof cell === 'object';
33
+
34
+ /* Whether a cell is only the start of its value, so editing it needs the whole value first. */
35
+ export const isPreview = (cell: Shown): boolean => {
36
+ if (!isObject(cell)) {
37
+ return false;
38
+ }
39
+ if (cell.kind === 'longText') {
40
+ return true;
41
+ }
42
+ return cell.kind === 'binary' && cell.hex.length < cell.length * 2;
43
+ };
44
+
45
+ /* A numeral in the notation asked for; text that is no numeral stays as it is. */
46
+ export const numeralText = (numeral: string | number, notation: NumberNotation): string => {
47
+ if (notation === 'database') {
48
+ return String(numeral);
49
+ }
50
+ const formatted = formatNumeral(numeral);
51
+ return formatted === String(numeral).trim() ? String(numeral) : formatted;
52
+ };
53
+
54
+ const cut = (text: string): string => (text.length > DISPLAY_LIMIT ? `${text.slice(0, DISPLAY_LIMIT)}${ELLIPSIS}` : text);
55
+
56
+ export const cellView = (cell: Shown, kind: ValueKind, notation: NumberNotation = 'database'): CellView => {
57
+ const numeric = isNumericKind(kind);
58
+ const align = numeric ? 'end' : 'start';
59
+ if (cell === null) {
60
+ return { text: 'NULL', tone: 'null', align };
61
+ }
62
+ if (typeof cell === 'boolean') {
63
+ return { text: cell ? 'true' : 'false', tone: 'value', align };
64
+ }
65
+ if (typeof cell === 'number') {
66
+ return { text: numeric ? numeralText(cell, notation) : String(cell), tone: 'value', align };
67
+ }
68
+ if (typeof cell === 'string') {
69
+ return { text: cut(numeric ? numeralText(cell, notation) : cell), tone: 'value', align };
70
+ }
71
+ switch (cell.kind) {
72
+ case 'default':
73
+ return { text: 'DEFAULT', tone: 'default', align };
74
+ case 'longText':
75
+ return { text: `${cut(cell.preview)}${cell.preview.length <= DISPLAY_LIMIT ? ELLIPSIS : ''}`, tone: 'value', align: 'start' };
76
+ case 'binary': {
77
+ const shown = cell.hex.slice(0, BINARY_PREVIEW_BYTES * 2);
78
+ const truncated = shown.length < cell.length * 2;
79
+ return { text: `0x${shown}${truncated ? ELLIPSIS : ''}`, tone: 'value', align: 'start', suffix: formatBytes(cell.length) };
80
+ }
81
+ }
82
+ };
83
+
84
+ /* The text a copy puts on the clipboard: the value as the cell has it, a preview as far as it goes. */
85
+ export const copyTextOf = (cell: Shown): string => {
86
+ if (cell === null) {
87
+ return 'NULL';
88
+ }
89
+ if (typeof cell !== 'object') {
90
+ return String(cell);
91
+ }
92
+ switch (cell.kind) {
93
+ case 'default':
94
+ return 'DEFAULT';
95
+ case 'longText':
96
+ return cell.preview;
97
+ case 'binary':
98
+ return `0x${cell.hex}`;
99
+ }
100
+ };
101
+
102
+ /* An edit as a cell, so the grid draws a pending value the way it draws a loaded one. */
103
+ export const shownOfEdit = (value: EditValue): Shown => {
104
+ if (value !== null && typeof value === 'object' && value.kind === 'binary') {
105
+ const binary = value as BinaryValue;
106
+ return { kind: 'binary', hex: binary.hex, length: binary.hex.length / 2 };
107
+ }
108
+ return value as Shown;
109
+ };
@@ -0,0 +1,60 @@
1
+ import type { Cell, EditValue, Value, ValueKind } from '../protocol/index.ts';
2
+
3
+ /* An empty field means NULL where an empty string cannot be a value of the column. */
4
+ const EMPTY_IS_NULL: ReadonlySet<ValueKind> = new Set(['integer', 'decimal', 'float', 'boolean', 'date', 'time', 'datetime', 'binary']);
5
+
6
+ const INTEGER = /^[+-]?\d+$/;
7
+ const HEX = /^0x([0-9a-f]*)$/i;
8
+
9
+ /* The text an editor starts with for a value that is whole (a preview has to be loaded first). */
10
+ export const draftOf = (value: Cell | Value | { readonly kind: 'default' }): string => {
11
+ if (value === null) {
12
+ return '';
13
+ }
14
+ if (typeof value !== 'object') {
15
+ return String(value);
16
+ }
17
+ if (value.kind === 'binary') {
18
+ return `0x${value.hex}`;
19
+ }
20
+ if (value.kind === 'longText') {
21
+ return value.preview;
22
+ }
23
+ return '';
24
+ };
25
+
26
+ /* What a person typed, as the value to send. A number the text spells exactly stays a number; anything else goes as text and the server decides. */
27
+ export const parseDraft = (text: string, kind: ValueKind): EditValue => {
28
+ const trimmed = text.trim();
29
+ if (trimmed === '' && EMPTY_IS_NULL.has(kind)) {
30
+ return null;
31
+ }
32
+ switch (kind) {
33
+ case 'integer': {
34
+ const number = Number(trimmed);
35
+ return INTEGER.test(trimmed) && Number.isSafeInteger(number) ? number : trimmed;
36
+ }
37
+ case 'float': {
38
+ const number = Number(trimmed);
39
+ return Number.isFinite(number) ? number : trimmed;
40
+ }
41
+ case 'boolean': {
42
+ const lower = trimmed.toLowerCase();
43
+ if (lower === 'true' || lower === '1') {
44
+ return true;
45
+ }
46
+ return lower === 'false' || lower === '0' ? false : trimmed;
47
+ }
48
+ case 'binary': {
49
+ const match = HEX.exec(trimmed);
50
+ return match !== null && match[1]!.length % 2 === 0 ? { kind: 'binary', hex: match[1]!.toLowerCase() } : trimmed;
51
+ }
52
+ case 'decimal':
53
+ case 'date':
54
+ case 'time':
55
+ case 'datetime':
56
+ return trimmed;
57
+ default:
58
+ return text;
59
+ }
60
+ };
@@ -0,0 +1,62 @@
1
+ export interface EnumType {
2
+ readonly kind: 'enum' | 'set';
3
+ readonly values: readonly string[];
4
+ }
5
+
6
+ const DECLARATION = /^(enum|set)\s*\(/i;
7
+
8
+ /*
9
+ * The values of a MySQL `enum('a','b')` or `set('a','b')` column type, or `null` for any other type
10
+ * or a list that does not parse. A quote inside a value is written doubled (`'it''s'`).
11
+ */
12
+ export const parseEnumType = (type: string): EnumType | null => {
13
+ const head = DECLARATION.exec(type.trim());
14
+ if (head === null) {
15
+ return null;
16
+ }
17
+ const source = type.trim().slice(head[0].length);
18
+ const values: string[] = [];
19
+ let position = 0;
20
+ while (position < source.length) {
21
+ if (source[position] !== "'") {
22
+ return null;
23
+ }
24
+ position++;
25
+ let value = '';
26
+ for (;;) {
27
+ if (position >= source.length) {
28
+ return null;
29
+ }
30
+ if (source[position] === "'") {
31
+ if (source[position + 1] === "'") {
32
+ value += "'";
33
+ position += 2;
34
+ continue;
35
+ }
36
+ position++;
37
+ break;
38
+ }
39
+ value += source[position];
40
+ position++;
41
+ }
42
+ values.push(value);
43
+ const next = source[position];
44
+ if (next === ')') {
45
+ return source.slice(position + 1).trim() === '' ? tidy(head[1]!, values) : null;
46
+ }
47
+ if (next !== ',') {
48
+ return null;
49
+ }
50
+ position++;
51
+ }
52
+ return null;
53
+ };
54
+
55
+ const tidy = (kind: string, values: string[]): EnumType | null =>
56
+ values.length === 0 ? null : { kind: kind.toLowerCase() === 'set' ? 'set' : 'enum', values };
57
+
58
+ /* The members a SET value holds: the text split on commas, empty for the empty set. */
59
+ export const membersOf = (value: string): string[] => (value === '' ? [] : value.split(','));
60
+
61
+ /* A SET value in the order of the column's values, which is the order the server writes it in. */
62
+ export const joinMembers = (type: EnumType, members: ReadonlySet<string>): string => type.values.filter((value) => members.has(value)).join(',');