@edge-base/server 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,1273 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/schema.ts
3
+ * 1-03 schema.test.ts — 80개
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/schema.test.ts
6
+ *
7
+ * 테스트 대상:
8
+ * buildEffectiveSchema — auto fields injection / user overrides / schemaless
9
+ * generateCreateTableDDL — column types + constraints
10
+ * generateAddColumnDDL — ADD COLUMN DDL
11
+ * generateIndexDDL — single/composite/unique
12
+ * generateFTS5DDL — FTS5 virtual table
13
+ * generateFTS5Triggers — INSERT/DELETE/UPDATE triggers
14
+ * computeSchemaHash / computeSchemaHashSync — deterministic
15
+ * generateTableDDL — full table DDL array
16
+ */
17
+
18
+ import { describe, it, expect } from 'vitest';
19
+ import {
20
+ buildEffectiveSchema,
21
+ generateCreateTableDDL,
22
+ generateAddColumnDDL,
23
+ generateIndexDDL,
24
+ generateFTS5DDL,
25
+ generateFTS5Triggers,
26
+ computeSchemaHash,
27
+ computeSchemaHashSync,
28
+ generateTableDDL,
29
+ META_TABLE_DDL,
30
+ // PostgreSQL DDL
31
+ PG_META_TABLE_DDL,
32
+ generatePgCreateTableDDL,
33
+ generatePgAddColumnDDL,
34
+ generatePgIndexDDL,
35
+ generatePgFTSDDL,
36
+ generatePgTableDDL,
37
+ } from '../lib/schema.js';
38
+ import { AUTH_D1_SCHEMA } from '../lib/auth-d1.js';
39
+ import { zodDefaultHook } from '../lib/schemas.js';
40
+ import type { TableConfig } from '@edge-base/shared';
41
+
42
+ // ─── A. buildEffectiveSchema ──────────────────────────────────────────────────
43
+
44
+ describe('buildEffectiveSchema', () => {
45
+ it('no user schema → auto fields only (id, createdAt, updatedAt)', () => {
46
+ const schema = buildEffectiveSchema(undefined);
47
+ expect(Object.keys(schema)).toContain('id');
48
+ expect(Object.keys(schema)).toContain('createdAt');
49
+ expect(Object.keys(schema)).toContain('updatedAt');
50
+ expect(Object.keys(schema)).toHaveLength(3);
51
+ });
52
+
53
+ it('user fields added alongside auto fields', () => {
54
+ const schema = buildEffectiveSchema({ title: { type: 'string' } });
55
+ expect(Object.keys(schema)).toContain('id');
56
+ expect(Object.keys(schema)).toContain('title');
57
+ });
58
+
59
+ it('user can set auto field to false to disable it', () => {
60
+ const schema = buildEffectiveSchema({ updatedAt: false });
61
+ expect(Object.keys(schema)).not.toContain('updatedAt');
62
+ expect(Object.keys(schema)).toContain('id');
63
+ expect(Object.keys(schema)).toContain('createdAt');
64
+ });
65
+
66
+ it('auto-field type override is ignored — always uses default type', () => {
67
+ // Even if user schema object is passed for an auto-field, the default type is used
68
+ // (defineConfig blocks this at validation, but buildEffectiveSchema also ignores overrides)
69
+ const schema = buildEffectiveSchema({ id: { type: 'number', primaryKey: true } });
70
+ // id in AUTO_FIELDS is always 'string' — user object treated as "present, not false" → default injected
71
+ expect(schema.id.type).toBe('string');
72
+ expect(schema.id.primaryKey).toBe(true);
73
+ });
74
+
75
+ it('multiple user fields all included', () => {
76
+ const schema = buildEffectiveSchema({
77
+ title: { type: 'string' },
78
+ views: { type: 'number' },
79
+ active: { type: 'boolean' },
80
+ });
81
+ expect(Object.keys(schema)).toContain('title');
82
+ expect(Object.keys(schema)).toContain('views');
83
+ expect(Object.keys(schema)).toContain('active');
84
+ });
85
+
86
+ it('disabling all auto fields', () => {
87
+ const schema = buildEffectiveSchema({ id: false, createdAt: false, updatedAt: false });
88
+ expect(Object.keys(schema)).toHaveLength(0);
89
+ });
90
+ });
91
+
92
+ // ─── B. generateCreateTableDDL ───────────────────────────────────────────────
93
+
94
+ describe('generateCreateTableDDL', () => {
95
+ it('generates CREATE TABLE IF NOT EXISTS', () => {
96
+ const ddl = generateCreateTableDDL('posts', { schema: {} } as TableConfig);
97
+ expect(ddl).toContain('CREATE TABLE IF NOT EXISTS');
98
+ expect(ddl).toContain('"posts"');
99
+ });
100
+
101
+ it('id column is PRIMARY KEY', () => {
102
+ const ddl = generateCreateTableDDL('posts', { schema: {} } as TableConfig);
103
+ expect(ddl).toContain('PRIMARY KEY');
104
+ });
105
+
106
+ it('string → TEXT type', () => {
107
+ const ddl = generateCreateTableDDL('t', { schema: { title: { type: 'string' } } } as TableConfig);
108
+ expect(ddl).toContain('"title" TEXT');
109
+ });
110
+
111
+ it('number → REAL type', () => {
112
+ const ddl = generateCreateTableDDL('t', { schema: { views: { type: 'number' } } } as TableConfig);
113
+ expect(ddl).toContain('"views" REAL');
114
+ });
115
+
116
+ it('boolean → INTEGER type', () => {
117
+ const ddl = generateCreateTableDDL('t', { schema: { active: { type: 'boolean' } } } as TableConfig);
118
+ expect(ddl).toContain('"active" INTEGER');
119
+ });
120
+
121
+ it('json → TEXT type', () => {
122
+ const ddl = generateCreateTableDDL('t', { schema: { data: { type: 'json' } } } as TableConfig);
123
+ expect(ddl).toContain('"data" TEXT');
124
+ });
125
+
126
+ it('datetime → TEXT type', () => {
127
+ const ddl = generateCreateTableDDL('t', { schema: { ts: { type: 'datetime' } } } as TableConfig);
128
+ expect(ddl).toContain('"ts" TEXT');
129
+ });
130
+
131
+ it('required field → NOT NULL', () => {
132
+ const ddl = generateCreateTableDDL('t', { schema: { email: { type: 'string', required: true } } } as TableConfig);
133
+ expect(ddl).toContain('NOT NULL');
134
+ });
135
+
136
+ it('unique field → UNIQUE', () => {
137
+ const ddl = generateCreateTableDDL('t', { schema: { slug: { type: 'string', unique: true } } } as TableConfig);
138
+ expect(ddl).toContain('UNIQUE');
139
+ });
140
+
141
+ it('default string value', () => {
142
+ const ddl = generateCreateTableDDL('t', { schema: { role: { type: 'string', default: 'user' } } } as TableConfig);
143
+ expect(ddl).toContain("DEFAULT 'user'");
144
+ });
145
+
146
+ it('default boolean value', () => {
147
+ const ddl = generateCreateTableDDL('t', { schema: { active: { type: 'boolean', default: true } } } as TableConfig);
148
+ expect(ddl).toContain('DEFAULT 1');
149
+ });
150
+
151
+ it('default null value', () => {
152
+ const ddl = generateCreateTableDDL('t', { schema: { deletedAt: { type: 'datetime', default: null } } } as TableConfig);
153
+ expect(ddl).toContain('DEFAULT NULL');
154
+ });
155
+
156
+ it('primary key field not marked NOT NULL (PK is implicit NOT NULL)', () => {
157
+ const ddl = generateCreateTableDDL('t', { schema: {} } as TableConfig);
158
+ // id is PRIMARY KEY, should not have duplicate NOT NULL
159
+ // The check: required && !primaryKey → NOT NULL
160
+ const idLine = ddl.split('\n').find((l) => l.includes('"id"'));
161
+ expect(idLine).toContain('PRIMARY KEY');
162
+ });
163
+ });
164
+
165
+ // ─── C. generateAddColumnDDL ─────────────────────────────────────────────────
166
+
167
+ describe('generateAddColumnDDL', () => {
168
+ it('generates ALTER TABLE ADD COLUMN', () => {
169
+ const ddl = generateAddColumnDDL('posts', 'rating', { type: 'number' });
170
+ expect(ddl).toContain('ALTER TABLE "posts" ADD COLUMN "rating" REAL');
171
+ });
172
+
173
+ it('with NOT NULL constraint', () => {
174
+ const ddl = generateAddColumnDDL('t', 'email', { type: 'string', required: true });
175
+ expect(ddl).toContain('NOT NULL');
176
+ });
177
+
178
+ it('with DEFAULT value', () => {
179
+ const ddl = generateAddColumnDDL('t', 'active', { type: 'boolean', default: false });
180
+ expect(ddl).toContain('DEFAULT 0');
181
+ });
182
+
183
+ it('ends with semicolon', () => {
184
+ const ddl = generateAddColumnDDL('t', 'col', { type: 'string' });
185
+ expect(ddl.trim()).toMatch(/;$/);
186
+ });
187
+ });
188
+
189
+ // ─── D. generateIndexDDL ─────────────────────────────────────────────────────
190
+
191
+ describe('generateIndexDDL', () => {
192
+ it('single-field index', () => {
193
+ const ddls = generateIndexDDL('posts', [{ fields: ['status'] }]);
194
+ expect(ddls).toHaveLength(1);
195
+ expect(ddls[0]).toContain('CREATE INDEX IF NOT EXISTS');
196
+ expect(ddls[0]).toContain('"posts"("status")');
197
+ });
198
+
199
+ it('unique index', () => {
200
+ const ddls = generateIndexDDL('posts', [{ fields: ['slug'], unique: true }]);
201
+ expect(ddls[0]).toContain('UNIQUE INDEX');
202
+ });
203
+
204
+ it('composite index (multi-field)', () => {
205
+ const ddls = generateIndexDDL('posts', [{ fields: ['userId', 'createdAt'] }]);
206
+ expect(ddls[0]).toContain('"userId"');
207
+ expect(ddls[0]).toContain('"createdAt"');
208
+ });
209
+
210
+ it('index name derived from table + fields', () => {
211
+ const ddls = generateIndexDDL('posts', [{ fields: ['status'] }]);
212
+ expect(ddls[0]).toContain('idx_posts_status');
213
+ });
214
+
215
+ it('multiple indexes', () => {
216
+ const ddls = generateIndexDDL('t', [
217
+ { fields: ['a'] },
218
+ { fields: ['b', 'c'] },
219
+ ]);
220
+ expect(ddls).toHaveLength(2);
221
+ });
222
+
223
+ it('empty indexes → empty array', () => {
224
+ const ddls = generateIndexDDL('t', []);
225
+ expect(ddls).toEqual([]);
226
+ });
227
+ });
228
+
229
+ // ─── E. generateFTS5DDL ────────────────────────────────────────────────────
230
+
231
+ describe('generateFTS5DDL', () => {
232
+ it('creates virtual table with _fts suffix', () => {
233
+ const ddl = generateFTS5DDL('posts', ['title', 'body']);
234
+ expect(ddl).toContain('"posts_fts"');
235
+ expect(ddl).toContain('CREATE VIRTUAL TABLE IF NOT EXISTS');
236
+ });
237
+
238
+ it('uses fts5 engine', () => {
239
+ const ddl = generateFTS5DDL('posts', ['title']);
240
+ expect(ddl).toContain('USING fts5');
241
+ });
242
+
243
+ it('includes content table reference', () => {
244
+ const ddl = generateFTS5DDL('posts', ['title']);
245
+ expect(ddl).toContain("content='posts'");
246
+ });
247
+
248
+ it('uses trigram tokenizer', () => {
249
+ const ddl = generateFTS5DDL('posts', ['title']);
250
+ expect(ddl).toContain("tokenize='trigram'");
251
+ });
252
+
253
+ it('includes all FTS fields', () => {
254
+ const ddl = generateFTS5DDL('articles', ['title', 'content', 'tags']);
255
+ expect(ddl).toContain('title');
256
+ expect(ddl).toContain('content');
257
+ expect(ddl).toContain('tags');
258
+ });
259
+ });
260
+
261
+ // ─── F. generateFTS5Triggers ──────────────────────────────────────────────
262
+
263
+ describe('generateFTS5Triggers', () => {
264
+ it('generates 3 triggers (INSERT, DELETE, UPDATE)', () => {
265
+ const triggers = generateFTS5Triggers('posts', ['title']);
266
+ expect(triggers).toHaveLength(3);
267
+ });
268
+
269
+ it('INSERT trigger: AFTER INSERT', () => {
270
+ const triggers = generateFTS5Triggers('posts', ['title']);
271
+ expect(triggers[0]).toContain('AFTER INSERT ON "posts"');
272
+ });
273
+
274
+ it('DELETE trigger: AFTER DELETE', () => {
275
+ const triggers = generateFTS5Triggers('posts', ['title']);
276
+ expect(triggers[1]).toContain('AFTER DELETE ON "posts"');
277
+ });
278
+
279
+ it('UPDATE trigger: AFTER UPDATE', () => {
280
+ const triggers = generateFTS5Triggers('posts', ['title']);
281
+ expect(triggers[2]).toContain('AFTER UPDATE ON "posts"');
282
+ });
283
+
284
+ it('trigger names use _ai, _ad, _au suffixes', () => {
285
+ const triggers = generateFTS5Triggers('posts', ['title']);
286
+ expect(triggers[0]).toContain('posts_ai');
287
+ expect(triggers[1]).toContain('posts_ad');
288
+ expect(triggers[2]).toContain('posts_au');
289
+ });
290
+ });
291
+
292
+ // ─── G. computeSchemaHash (async SHA-256) ────────────────────────────────────
293
+
294
+ describe('computeSchemaHash', () => {
295
+ it('returns a hex string', async () => {
296
+ const hash = await computeSchemaHash({ schema: { title: { type: 'string' } } } as TableConfig);
297
+ expect(hash).toMatch(/^[0-9a-f]+$/);
298
+ });
299
+
300
+ it('deterministic — same input → same hash', async () => {
301
+ const cfg: TableConfig = { schema: { title: { type: 'string' } } } as TableConfig;
302
+ const h1 = await computeSchemaHash(cfg);
303
+ const h2 = await computeSchemaHash(cfg);
304
+ expect(h1).toBe(h2);
305
+ });
306
+
307
+ it('different schema → different hash', async () => {
308
+ const h1 = await computeSchemaHash({ schema: { a: { type: 'string' } } } as TableConfig);
309
+ const h2 = await computeSchemaHash({ schema: { b: { type: 'number' } } } as TableConfig);
310
+ expect(h1).not.toBe(h2);
311
+ });
312
+
313
+ it('SHA-256 produces 64-char hex', async () => {
314
+ const hash = await computeSchemaHash({} as TableConfig);
315
+ expect(hash).toHaveLength(64);
316
+ });
317
+ });
318
+
319
+ // ─── H. computeSchemaHashSync (djb2) ─────────────────────────────────────────
320
+
321
+ describe('computeSchemaHashSync', () => {
322
+ it('returns an 8-char hex string', () => {
323
+ const hash = computeSchemaHashSync({ schema: { title: { type: 'string' } } } as TableConfig);
324
+ expect(hash).toMatch(/^[0-9a-f]{8}$/);
325
+ });
326
+
327
+ it('deterministic with same schema', () => {
328
+ const cfg = { schema: { a: { type: 'string' } } } as TableConfig;
329
+ expect(computeSchemaHashSync(cfg)).toBe(computeSchemaHashSync(cfg));
330
+ });
331
+
332
+ it('different schema → different hash', () => {
333
+ const h1 = computeSchemaHashSync({ schema: { x: { type: 'string' } } } as TableConfig);
334
+ const h2 = computeSchemaHashSync({ schema: { y: { type: 'number' } } } as TableConfig);
335
+ expect(h1).not.toBe(h2);
336
+ });
337
+
338
+ it('access field ignored (functions serialize to undefined)', () => {
339
+ // Same schema field, different access functions → same hash
340
+ const h1 = computeSchemaHashSync({ schema: { a: { type: 'string' } } } as TableConfig);
341
+ const h2 = computeSchemaHashSync({
342
+ schema: { a: { type: 'string' } },
343
+ access: { read: () => true },
344
+ } as unknown as TableConfig);
345
+ expect(h1).toBe(h2);
346
+ });
347
+ });
348
+
349
+ // ─── I. generateTableDDL ─────────────────────────────────────────────────────
350
+
351
+ describe('generateTableDDL', () => {
352
+ it('returns array with CREATE TABLE', () => {
353
+ const ddls = generateTableDDL('posts', { schema: {} } as TableConfig);
354
+ expect(ddls.length).toBeGreaterThanOrEqual(1);
355
+ expect(ddls[0]).toContain('CREATE TABLE');
356
+ });
357
+
358
+ it('with indexes → includes CREATE INDEX', () => {
359
+ const ddls = generateTableDDL('posts', {
360
+ schema: {},
361
+ indexes: [{ fields: ['status'] }],
362
+ } as TableConfig);
363
+ expect(ddls.some((d) => d.includes('CREATE INDEX'))).toBe(true);
364
+ });
365
+
366
+ it('with FTS → includes CREATE VIRTUAL TABLE + 3 triggers', () => {
367
+ const ddls = generateTableDDL('posts', {
368
+ schema: { title: { type: 'string' } },
369
+ fts: ['title'],
370
+ } as TableConfig);
371
+ expect(ddls.some((d) => d.includes('VIRTUAL TABLE'))).toBe(true);
372
+ // 1 CREATE TABLE + 1 FTS + 3 triggers = 5
373
+ expect(ddls.length).toBeGreaterThanOrEqual(4);
374
+ });
375
+
376
+ it('no FTS → no triggers', () => {
377
+ const ddls = generateTableDDL('posts', { schema: {} } as TableConfig);
378
+ expect(ddls.every((d) => !d.includes('TRIGGER'))).toBe(true);
379
+ });
380
+ });
381
+
382
+ // ─── J. System DDL constants ──────────────────────────────────────────────────
383
+
384
+ describe('system DDL constants', () => {
385
+ it('META_TABLE_DDL creates _meta table', () => {
386
+ expect(META_TABLE_DDL).toContain('_meta');
387
+ expect(META_TABLE_DDL).toContain('key TEXT PRIMARY KEY');
388
+ });
389
+
390
+ it('AUTH_D1_SCHEMA creates _users_public table', () => {
391
+ expect(AUTH_D1_SCHEMA).toContain('_users_public');
392
+ expect(AUTH_D1_SCHEMA).toContain('email');
393
+ expect(AUTH_D1_SCHEMA).toContain('CREATE INDEX');
394
+ });
395
+
396
+ it('AUTH_D1_SCHEMA no longer defines _schedules table', () => {
397
+ expect(AUTH_D1_SCHEMA).not.toContain('_schedules');
398
+ });
399
+ });
400
+
401
+ // ─── K. Edge cases (mutation coverage) ──────────────────────────────────────
402
+
403
+ describe('buildEffectiveSchema — edge cases', () => {
404
+ it('user field set to false (non-auto) → excluded', () => {
405
+ const schema = buildEffectiveSchema({ title: false, body: { type: 'string' } });
406
+ expect(Object.keys(schema)).not.toContain('title');
407
+ expect(Object.keys(schema)).toContain('body');
408
+ });
409
+
410
+ it('empty user schema → auto fields only', () => {
411
+ const schema = buildEffectiveSchema({});
412
+ expect(Object.keys(schema)).toEqual(['id', 'createdAt', 'updatedAt']);
413
+ });
414
+ });
415
+
416
+ describe('generateCreateTableDDL — edge cases', () => {
417
+ it('unknown field type → fallback to TEXT', () => {
418
+ const ddl = generateCreateTableDDL('t', {
419
+ schema: { exotic: { type: 'vector' as any } },
420
+ } as TableConfig);
421
+ expect(ddl).toContain('"exotic" TEXT');
422
+ });
423
+
424
+ it('required + primaryKey → only PRIMARY KEY (no NOT NULL)', () => {
425
+ const ddl = generateCreateTableDDL('t', {
426
+ schema: { pk: { type: 'string', primaryKey: true, required: true } },
427
+ } as TableConfig);
428
+ const pkLine = ddl.split('\n').find((l) => l.includes('"pk"'))!;
429
+ expect(pkLine).toContain('PRIMARY KEY');
430
+ // required + primaryKey should NOT add NOT NULL (it's implicit)
431
+ expect(pkLine).not.toContain('NOT NULL');
432
+ });
433
+
434
+ it('default number value', () => {
435
+ const ddl = generateCreateTableDDL('t', {
436
+ schema: { count: { type: 'number', default: 42 } },
437
+ } as TableConfig);
438
+ expect(ddl).toContain('DEFAULT 42');
439
+ });
440
+
441
+ it('default false boolean → DEFAULT 0', () => {
442
+ const ddl = generateCreateTableDDL('t', {
443
+ schema: { active: { type: 'boolean', default: false } },
444
+ } as TableConfig);
445
+ expect(ddl).toContain('DEFAULT 0');
446
+ });
447
+
448
+ it('string default with single quote → escaped', () => {
449
+ const ddl = generateCreateTableDDL('t', {
450
+ schema: { note: { type: 'string', default: "it's" } },
451
+ } as TableConfig);
452
+ expect(ddl).toContain("DEFAULT 'it''s'");
453
+ });
454
+
455
+ it('field with auth user references string form stays logical only', () => {
456
+ const ddl = generateCreateTableDDL('t', {
457
+ schema: { userId: { type: 'string', references: 'users' } },
458
+ } as TableConfig);
459
+ expect(ddl).not.toContain('REFERENCES "users"("id")');
460
+ });
461
+
462
+ it('field with auth references string form (with column) stays logical only', () => {
463
+ const ddl = generateCreateTableDDL('t', {
464
+ schema: { userId: { type: 'string', references: 'users(uid)' } },
465
+ } as TableConfig);
466
+ expect(ddl).not.toContain('REFERENCES "users"("uid")');
467
+ });
468
+
469
+ it('field with non-auth references string form emits a physical FK', () => {
470
+ const ddl = generateCreateTableDDL('t', {
471
+ schema: { orderId: { type: 'string', references: 'orders' } },
472
+ } as TableConfig);
473
+ expect(ddl).toContain('REFERENCES "orders"("id") ON DELETE SET NULL');
474
+ });
475
+
476
+ it('field with references object form', () => {
477
+ const ddl = generateCreateTableDDL('t', {
478
+ schema: {
479
+ categoryId: {
480
+ type: 'string',
481
+ references: { table: 'categories', column: 'cid', onDelete: 'CASCADE', onUpdate: 'SET NULL' },
482
+ },
483
+ },
484
+ } as TableConfig);
485
+ expect(ddl).toContain('REFERENCES "categories"("cid")');
486
+ expect(ddl).toContain('ON DELETE CASCADE');
487
+ expect(ddl).toContain('ON UPDATE SET NULL');
488
+ });
489
+
490
+ it('field with references object form (default column)', () => {
491
+ const ddl = generateCreateTableDDL('t', {
492
+ schema: {
493
+ postId: { type: 'string', references: { table: 'posts' } },
494
+ },
495
+ } as TableConfig);
496
+ expect(ddl).toContain('REFERENCES "posts"("id")');
497
+ });
498
+
499
+ it('field with check constraint', () => {
500
+ const ddl = generateCreateTableDDL('t', {
501
+ schema: { age: { type: 'number', check: 'age >= 0' } },
502
+ } as TableConfig);
503
+ expect(ddl).toContain('CHECK (age >= 0)');
504
+ });
505
+
506
+ it('identifier with double-quote → escaped', () => {
507
+ const ddl = generateCreateTableDDL('my"table', { schema: {} } as TableConfig);
508
+ expect(ddl).toContain('"my""table"');
509
+ });
510
+ });
511
+
512
+ describe('computeSchemaHashSync — edge cases', () => {
513
+ it('empty schema → consistent hash', () => {
514
+ const h1 = computeSchemaHashSync({} as TableConfig);
515
+ const h2 = computeSchemaHashSync({} as TableConfig);
516
+ expect(h1).toBe(h2);
517
+ expect(h1).toMatch(/^[0-9a-f]{8}$/);
518
+ });
519
+
520
+ it('key ordering does not matter (deep sorted)', () => {
521
+ const h1 = computeSchemaHashSync({
522
+ schema: { a: { type: 'string' }, b: { type: 'number' } },
523
+ } as TableConfig);
524
+ const h2 = computeSchemaHashSync({
525
+ schema: { b: { type: 'number' }, a: { type: 'string' } },
526
+ } as TableConfig);
527
+ expect(h1).toBe(h2);
528
+ });
529
+ });
530
+
531
+ describe('generateTableDDL — edge cases', () => {
532
+ it('no indexes, no fts → only CREATE TABLE', () => {
533
+ const ddls = generateTableDDL('simple', { schema: {} } as TableConfig);
534
+ expect(ddls).toHaveLength(1);
535
+ expect(ddls[0]).toContain('CREATE TABLE');
536
+ });
537
+
538
+ it('indexes + fts combined', () => {
539
+ const ddls = generateTableDDL('full', {
540
+ schema: { title: { type: 'string' }, status: { type: 'string' } },
541
+ indexes: [{ fields: ['status'] }],
542
+ fts: ['title'],
543
+ } as TableConfig);
544
+ // 1 CREATE TABLE + 1 INDEX + 1 FTS + 3 triggers = 6
545
+ expect(ddls).toHaveLength(6);
546
+ });
547
+
548
+ it('empty indexes array → no CREATE INDEX', () => {
549
+ const ddls = generateTableDDL('t', {
550
+ schema: {},
551
+ indexes: [],
552
+ } as TableConfig);
553
+ expect(ddls.every((d) => !d.includes('CREATE INDEX'))).toBe(true);
554
+ });
555
+
556
+ it('empty fts array → no FTS', () => {
557
+ const ddls = generateTableDDL('t', {
558
+ schema: {},
559
+ fts: [],
560
+ } as TableConfig);
561
+ expect(ddls.every((d) => !d.includes('VIRTUAL TABLE'))).toBe(true);
562
+ });
563
+ });
564
+
565
+ describe('generateFTS5DDL — edge cases', () => {
566
+ it('single field', () => {
567
+ const ddl = generateFTS5DDL('t', ['body']);
568
+ expect(ddl).toContain('body');
569
+ expect(ddl).toContain("content='t'");
570
+ });
571
+ });
572
+
573
+ describe('generateFTS5Triggers — edge cases', () => {
574
+ it('multiple fields → all included in triggers', () => {
575
+ const triggers = generateFTS5Triggers('t', ['title', 'body', 'tags']);
576
+ // INSERT trigger should reference all new.fields
577
+ expect(triggers[0]).toContain('new."title"');
578
+ expect(triggers[0]).toContain('new."body"');
579
+ expect(triggers[0]).toContain('new."tags"');
580
+ // DELETE trigger should reference all old.fields
581
+ expect(triggers[1]).toContain('old."title"');
582
+ expect(triggers[1]).toContain('old."body"');
583
+ expect(triggers[1]).toContain('old."tags"');
584
+ });
585
+ });
586
+
587
+ // ─── Mutation-killing: schema precision tests ──────────────────────────────
588
+
589
+ describe('buildColumnDef — mutation-killing', () => {
590
+ it('primaryKey → DDL includes PRIMARY KEY', () => {
591
+ const ddl = generateCreateTableDDL('t', {
592
+ schema: { myid: { type: 'string', primaryKey: true } },
593
+ } as TableConfig);
594
+ expect(ddl).toContain('"myid" TEXT PRIMARY KEY');
595
+ });
596
+
597
+ it('required non-PK → DDL includes NOT NULL', () => {
598
+ const ddl = generateCreateTableDDL('t', {
599
+ schema: { name: { type: 'string', required: true } },
600
+ } as TableConfig);
601
+ expect(ddl).toContain('NOT NULL');
602
+ });
603
+
604
+ it('required + PK → DDL has PRIMARY KEY but NOT "NOT NULL"', () => {
605
+ const ddl = generateCreateTableDDL('t', {
606
+ schema: { myid: { type: 'string', primaryKey: true, required: true } },
607
+ } as TableConfig);
608
+ expect(ddl).toContain('PRIMARY KEY');
609
+ // PK fields should not also have NOT NULL (redundant in SQLite)
610
+ expect(ddl).not.toMatch(/"myid".*NOT NULL/);
611
+ });
612
+
613
+ it('unique → DDL includes UNIQUE', () => {
614
+ const ddl = generateCreateTableDDL('t', {
615
+ schema: { email: { type: 'string', unique: true } },
616
+ } as TableConfig);
617
+ expect(ddl).toContain('UNIQUE');
618
+ });
619
+
620
+ it('default string → DDL includes DEFAULT with escaped quotes', () => {
621
+ const ddl = generateCreateTableDDL('t', {
622
+ schema: { status: { type: 'string', default: "it's" } },
623
+ } as TableConfig);
624
+ expect(ddl).toContain("DEFAULT 'it''s'");
625
+ });
626
+
627
+ it('default number → DDL includes DEFAULT with number value', () => {
628
+ const ddl = generateCreateTableDDL('t', {
629
+ schema: { count: { type: 'number', default: 42 } },
630
+ } as TableConfig);
631
+ expect(ddl).toContain('DEFAULT 42');
632
+ });
633
+
634
+ it('default boolean true → DEFAULT 1', () => {
635
+ const ddl = generateCreateTableDDL('t', {
636
+ schema: { active: { type: 'boolean', default: true } },
637
+ } as TableConfig);
638
+ expect(ddl).toContain('DEFAULT 1');
639
+ });
640
+
641
+ it('default boolean false → DEFAULT 0', () => {
642
+ const ddl = generateCreateTableDDL('t', {
643
+ schema: { active: { type: 'boolean', default: false } },
644
+ } as TableConfig);
645
+ expect(ddl).toContain('DEFAULT 0');
646
+ });
647
+
648
+ it('default null → DEFAULT NULL', () => {
649
+ const ddl = generateCreateTableDDL('t', {
650
+ schema: { notes: { type: 'string', default: null } },
651
+ } as TableConfig);
652
+ expect(ddl).toContain('DEFAULT NULL');
653
+ });
654
+
655
+ it('references string "users(col)" form stays logical only', () => {
656
+ const ddl = generateCreateTableDDL('t', {
657
+ schema: { userId: { type: 'string', references: 'users(id)' } },
658
+ } as TableConfig);
659
+ expect(ddl).not.toContain('REFERENCES "users"("id")');
660
+ });
661
+
662
+ it('references plain auth table name stays logical only', () => {
663
+ const ddl = generateCreateTableDDL('t', {
664
+ schema: { userId: { type: 'string', references: 'users' } },
665
+ } as TableConfig);
666
+ expect(ddl).not.toContain('REFERENCES "users"("id")');
667
+ });
668
+
669
+ it('references plain non-auth table name emits a physical FK', () => {
670
+ const ddl = generateCreateTableDDL('t', {
671
+ schema: { orderId: { type: 'string', references: 'orders' } },
672
+ } as TableConfig);
673
+ expect(ddl).toContain('REFERENCES "orders"("id") ON DELETE SET NULL');
674
+ });
675
+
676
+ it('references auth object form stays logical only', () => {
677
+ const ddl = generateCreateTableDDL('t', {
678
+ schema: {
679
+ userId: {
680
+ type: 'string',
681
+ references: { table: 'users', column: 'uid', onDelete: 'CASCADE', onUpdate: 'SET NULL' },
682
+ },
683
+ },
684
+ } as TableConfig);
685
+ expect(ddl).not.toContain('REFERENCES "users"("uid")');
686
+ });
687
+
688
+ it('check constraint → DDL includes CHECK', () => {
689
+ const ddl = generateCreateTableDDL('t', {
690
+ schema: { age: { type: 'number', check: 'age >= 0' } },
691
+ } as TableConfig);
692
+ expect(ddl).toContain('CHECK (age >= 0)');
693
+ });
694
+ });
695
+
696
+ describe('buildEffectiveSchema — mutation-killing', () => {
697
+ it('auto-fields are copies (not references)', () => {
698
+ const schema1 = buildEffectiveSchema();
699
+ const schema2 = buildEffectiveSchema();
700
+ // Modifying one should not affect the other
701
+ schema1.id.type = 'number' as any;
702
+ expect(schema2.id.type).toBe('string');
703
+ });
704
+
705
+ it('columns joined with comma-newline-indent in DDL', () => {
706
+ const ddl = generateCreateTableDDL('t', {
707
+ schema: { a: { type: 'string' }, b: { type: 'number' } },
708
+ } as TableConfig);
709
+ // Verify columns are separated properly
710
+ expect(ddl).toContain(',\n ');
711
+ });
712
+ });
713
+
714
+ describe('generateIndexDDL — mutation-killing', () => {
715
+ it('composite index fields joined with comma-space', () => {
716
+ const ddls = generateIndexDDL('t', [{ fields: ['a', 'b', 'c'] }]);
717
+ expect(ddls[0]).toContain('"a", "b", "c"');
718
+ });
719
+
720
+ it('index name uses underscore between field names', () => {
721
+ const ddls = generateIndexDDL('t', [{ fields: ['first', 'last'] }]);
722
+ expect(ddls[0]).toContain('"idx_t_first_last"');
723
+ });
724
+
725
+ it('unique index includes UNIQUE keyword', () => {
726
+ const ddls = generateIndexDDL('t', [{ fields: ['email'], unique: true }]);
727
+ expect(ddls[0]).toContain('CREATE UNIQUE INDEX');
728
+ });
729
+
730
+ it('non-unique index has no UNIQUE keyword', () => {
731
+ const ddls = generateIndexDDL('t', [{ fields: ['status'] }]);
732
+ expect(ddls[0]).not.toContain('UNIQUE');
733
+ });
734
+ });
735
+
736
+ describe('generateFTS5DDL — mutation-killing', () => {
737
+ it('FTS table name is tableName_fts', () => {
738
+ const ddl = generateFTS5DDL('posts', ['title']);
739
+ expect(ddl).toContain('"posts_fts"');
740
+ });
741
+
742
+ it('fields listed in fts5() definition', () => {
743
+ const ddl = generateFTS5DDL('t', ['title', 'body']);
744
+ expect(ddl).toContain('fts5(title, body,');
745
+ });
746
+
747
+ it("content sync references base table with content='tableName'", () => {
748
+ const ddl = generateFTS5DDL('posts', ['title']);
749
+ expect(ddl).toContain("content='posts'");
750
+ });
751
+ });
752
+
753
+ describe('generateFTS5Triggers — mutation-killing', () => {
754
+ it('INSERT trigger name format: tableName_ai', () => {
755
+ const triggers = generateFTS5Triggers('posts', ['title']);
756
+ expect(triggers[0]).toContain('"posts_ai"');
757
+ });
758
+
759
+ it('DELETE trigger name format: tableName_ad', () => {
760
+ const triggers = generateFTS5Triggers('posts', ['title']);
761
+ expect(triggers[1]).toContain('"posts_ad"');
762
+ });
763
+
764
+ it('UPDATE trigger name format: tableName_au', () => {
765
+ const triggers = generateFTS5Triggers('posts', ['title']);
766
+ expect(triggers[2]).toContain('"posts_au"');
767
+ });
768
+
769
+ it('DELETE trigger inserts "delete" marker', () => {
770
+ const triggers = generateFTS5Triggers('posts', ['title']);
771
+ expect(triggers[1]).toContain("'delete'");
772
+ });
773
+
774
+ it('UPDATE trigger has delete-then-insert pattern', () => {
775
+ const triggers = generateFTS5Triggers('posts', ['title']);
776
+ expect(triggers[2]).toContain("'delete'");
777
+ expect(triggers[2]).toContain('new."title"');
778
+ });
779
+
780
+ it('field list in triggers uses comma-space separator', () => {
781
+ const triggers = generateFTS5Triggers('t', ['a', 'b']);
782
+ expect(triggers[0]).toContain('"a", "b"');
783
+ });
784
+ });
785
+
786
+ describe('computeSchemaHashSync — mutation-killing', () => {
787
+ it('different schemas produce different hashes', () => {
788
+ const h1 = computeSchemaHashSync({ schema: { a: { type: 'string' } } } as TableConfig);
789
+ const h2 = computeSchemaHashSync({ schema: { b: { type: 'number' } } } as TableConfig);
790
+ expect(h1).not.toBe(h2);
791
+ });
792
+
793
+ it('hash is 8-character hex string', () => {
794
+ const h = computeSchemaHashSync({ schema: {} } as TableConfig);
795
+ expect(h).toMatch(/^[0-9a-f]{8}$/);
796
+ });
797
+
798
+ it('key order does not affect hash (deep sort)', () => {
799
+ const h1 = computeSchemaHashSync({ schema: { a: { type: 'string' }, b: { type: 'number' } } } as TableConfig);
800
+ const h2 = computeSchemaHashSync({ schema: { b: { type: 'number' }, a: { type: 'string' } } } as TableConfig);
801
+ expect(h1).toBe(h2);
802
+ });
803
+ });
804
+
805
+ // ─── L. zodDefaultHook (schemas.ts) ──────────────────────────────────────────
806
+
807
+ describe('zodDefaultHook', () => {
808
+ function mockContext() {
809
+ let lastJson: unknown;
810
+ let lastStatus: number;
811
+ return {
812
+ json: (data: unknown, status: number) => { lastJson = data; lastStatus = status; return { data, status }; },
813
+ get lastJson() { return lastJson; },
814
+ get lastStatus() { return lastStatus; },
815
+ };
816
+ }
817
+
818
+ it('returns nothing on success', () => {
819
+ const c = mockContext();
820
+ const result = zodDefaultHook({ success: true }, c);
821
+ expect(result).toBeUndefined();
822
+ });
823
+
824
+ it('returns 400 with joined issue messages', () => {
825
+ const c = mockContext();
826
+ const result = zodDefaultHook({
827
+ success: false,
828
+ error: { issues: [{ message: 'field required' }, { message: 'invalid type' }] },
829
+ }, c);
830
+ expect(result).toBeDefined();
831
+ expect(c.lastJson).toEqual({ code: 400, message: 'field required, invalid type' });
832
+ expect(c.lastStatus).toBe(400);
833
+ });
834
+
835
+ it('handles Zod v3 errors array', () => {
836
+ const c = mockContext();
837
+ zodDefaultHook({
838
+ success: false,
839
+ error: { errors: [{ message: 'too short' }] },
840
+ }, c);
841
+ expect(c.lastJson).toEqual({ code: 400, message: 'too short' });
842
+ });
843
+
844
+ it('handles empty issues → empty message', () => {
845
+ const c = mockContext();
846
+ zodDefaultHook({
847
+ success: false,
848
+ error: { issues: [] },
849
+ }, c);
850
+ expect(c.lastJson).toEqual({ code: 400, message: '' });
851
+ });
852
+
853
+ it('handles missing error.issues and error.errors', () => {
854
+ const c = mockContext();
855
+ zodDefaultHook({
856
+ success: false,
857
+ error: {},
858
+ }, c);
859
+ expect(c.lastJson).toEqual({ code: 400, message: '' });
860
+ });
861
+
862
+ it('handles undefined error', () => {
863
+ const c = mockContext();
864
+ zodDefaultHook({
865
+ success: false,
866
+ }, c);
867
+ expect(c.lastJson).toEqual({ code: 400, message: '' });
868
+ });
869
+ });
870
+
871
+ // ═══════════════════════════════════════════════════════════════════════════
872
+ // PostgreSQL DDL Tests
873
+ // ═══════════════════════════════════════════════════════════════════════════
874
+
875
+ // ─── M. PG_META_TABLE_DDL ───────────────────────────────────────────────────
876
+
877
+ describe('PG_META_TABLE_DDL', () => {
878
+ it('creates _meta table', () => {
879
+ expect(PG_META_TABLE_DDL).toContain('_meta');
880
+ expect(PG_META_TABLE_DDL).toContain('key TEXT PRIMARY KEY');
881
+ expect(PG_META_TABLE_DDL).toContain('value TEXT NOT NULL');
882
+ });
883
+ });
884
+
885
+ // ─── N. generatePgCreateTableDDL ────────────────────────────────────────────
886
+
887
+ describe('generatePgCreateTableDDL', () => {
888
+ it('generates CREATE TABLE IF NOT EXISTS', () => {
889
+ const ddl = generatePgCreateTableDDL('posts', { schema: {} } as TableConfig);
890
+ expect(ddl).toContain('CREATE TABLE IF NOT EXISTS');
891
+ expect(ddl).toContain('"posts"');
892
+ });
893
+
894
+ it('id column is PRIMARY KEY', () => {
895
+ const ddl = generatePgCreateTableDDL('posts', { schema: {} } as TableConfig);
896
+ expect(ddl).toContain('"id" TEXT PRIMARY KEY');
897
+ });
898
+
899
+ it('string → TEXT type', () => {
900
+ const ddl = generatePgCreateTableDDL('t', { schema: { title: { type: 'string' } } } as TableConfig);
901
+ expect(ddl).toContain('"title" TEXT');
902
+ });
903
+
904
+ it('number → DOUBLE PRECISION type', () => {
905
+ const ddl = generatePgCreateTableDDL('t', { schema: { views: { type: 'number' } } } as TableConfig);
906
+ expect(ddl).toContain('"views" DOUBLE PRECISION');
907
+ });
908
+
909
+ it('boolean → BOOLEAN type (not INTEGER)', () => {
910
+ const ddl = generatePgCreateTableDDL('t', { schema: { active: { type: 'boolean' } } } as TableConfig);
911
+ expect(ddl).toContain('"active" BOOLEAN');
912
+ expect(ddl).not.toContain('INTEGER');
913
+ });
914
+
915
+ it('json → JSONB type (not TEXT)', () => {
916
+ const ddl = generatePgCreateTableDDL('t', { schema: { data: { type: 'json' } } } as TableConfig);
917
+ expect(ddl).toContain('"data" JSONB');
918
+ });
919
+
920
+ it('datetime → TIMESTAMPTZ type (not TEXT)', () => {
921
+ const ddl = generatePgCreateTableDDL('t', { schema: { ts: { type: 'datetime' } } } as TableConfig);
922
+ expect(ddl).toContain('"ts" TIMESTAMPTZ');
923
+ });
924
+
925
+ it('required field → NOT NULL', () => {
926
+ const ddl = generatePgCreateTableDDL('t', { schema: { email: { type: 'string', required: true } } } as TableConfig);
927
+ expect(ddl).toContain('NOT NULL');
928
+ });
929
+
930
+ it('unique field → UNIQUE', () => {
931
+ const ddl = generatePgCreateTableDDL('t', { schema: { slug: { type: 'string', unique: true } } } as TableConfig);
932
+ expect(ddl).toContain('UNIQUE');
933
+ });
934
+
935
+ it('default string value', () => {
936
+ const ddl = generatePgCreateTableDDL('t', { schema: { role: { type: 'string', default: 'user' } } } as TableConfig);
937
+ expect(ddl).toContain("DEFAULT 'user'");
938
+ });
939
+
940
+ it('default boolean true → TRUE (not 1)', () => {
941
+ const ddl = generatePgCreateTableDDL('t', { schema: { active: { type: 'boolean', default: true } } } as TableConfig);
942
+ expect(ddl).toContain('DEFAULT TRUE');
943
+ expect(ddl).not.toContain('DEFAULT 1');
944
+ });
945
+
946
+ it('default boolean false → FALSE (not 0)', () => {
947
+ const ddl = generatePgCreateTableDDL('t', { schema: { active: { type: 'boolean', default: false } } } as TableConfig);
948
+ expect(ddl).toContain('DEFAULT FALSE');
949
+ expect(ddl).not.toContain('DEFAULT 0');
950
+ });
951
+
952
+ it('default null value', () => {
953
+ const ddl = generatePgCreateTableDDL('t', { schema: { deletedAt: { type: 'datetime', default: null } } } as TableConfig);
954
+ expect(ddl).toContain('DEFAULT NULL');
955
+ });
956
+
957
+ it('default number value', () => {
958
+ const ddl = generatePgCreateTableDDL('t', { schema: { count: { type: 'number', default: 42 } } } as TableConfig);
959
+ expect(ddl).toContain('DEFAULT 42');
960
+ });
961
+
962
+ it('single-quote escape in default', () => {
963
+ const ddl = generatePgCreateTableDDL('t', { schema: { note: { type: 'string', default: "it's" } } } as TableConfig);
964
+ expect(ddl).toContain("DEFAULT 'it''s'");
965
+ });
966
+
967
+ it('primary key field not marked NOT NULL (implicit)', () => {
968
+ const ddl = generatePgCreateTableDDL('t', {
969
+ schema: { pk: { type: 'string', primaryKey: true, required: true } },
970
+ } as TableConfig);
971
+ expect(ddl).toContain('PRIMARY KEY');
972
+ expect(ddl).not.toMatch(/"pk".*NOT NULL/);
973
+ });
974
+
975
+ it('references auth string form (no column) stays logical only', () => {
976
+ const ddl = generatePgCreateTableDDL('t', {
977
+ schema: { userId: { type: 'string', references: 'users' } },
978
+ } as TableConfig);
979
+ expect(ddl).not.toContain('REFERENCES "users"("id")');
980
+ });
981
+
982
+ it('references auth string form (with column) stays logical only', () => {
983
+ const ddl = generatePgCreateTableDDL('t', {
984
+ schema: { userId: { type: 'string', references: 'users(uid)' } },
985
+ } as TableConfig);
986
+ expect(ddl).not.toContain('REFERENCES "users"("uid")');
987
+ });
988
+
989
+ it('references object form', () => {
990
+ const ddl = generatePgCreateTableDDL('t', {
991
+ schema: {
992
+ categoryId: {
993
+ type: 'string',
994
+ references: { table: 'categories', column: 'cid', onDelete: 'CASCADE', onUpdate: 'SET NULL' },
995
+ },
996
+ },
997
+ } as TableConfig);
998
+ expect(ddl).toContain('REFERENCES "categories"("cid") ON DELETE CASCADE ON UPDATE SET NULL');
999
+ });
1000
+
1001
+ it('check constraint', () => {
1002
+ const ddl = generatePgCreateTableDDL('t', {
1003
+ schema: { age: { type: 'number', check: 'age >= 0' } },
1004
+ } as TableConfig);
1005
+ expect(ddl).toContain('CHECK (age >= 0)');
1006
+ });
1007
+
1008
+ it('unknown type → TEXT fallback', () => {
1009
+ const ddl = generatePgCreateTableDDL('t', {
1010
+ schema: { exotic: { type: 'vector' as any } },
1011
+ } as TableConfig);
1012
+ expect(ddl).toContain('"exotic" TEXT');
1013
+ });
1014
+
1015
+ it('identifier with double-quote → escaped', () => {
1016
+ const ddl = generatePgCreateTableDDL('my"table', { schema: {} } as TableConfig);
1017
+ expect(ddl).toContain('"my""table"');
1018
+ });
1019
+ });
1020
+
1021
+ // ─── O. generatePgAddColumnDDL ──────────────────────────────────────────────
1022
+
1023
+ describe('generatePgAddColumnDDL', () => {
1024
+ it('generates ALTER TABLE ADD COLUMN', () => {
1025
+ const ddl = generatePgAddColumnDDL('posts', 'rating', { type: 'number' });
1026
+ expect(ddl).toContain('ALTER TABLE "posts" ADD COLUMN "rating" DOUBLE PRECISION');
1027
+ });
1028
+
1029
+ it('with NOT NULL constraint', () => {
1030
+ const ddl = generatePgAddColumnDDL('t', 'email', { type: 'string', required: true });
1031
+ expect(ddl).toContain('NOT NULL');
1032
+ });
1033
+
1034
+ it('with boolean DEFAULT', () => {
1035
+ const ddl = generatePgAddColumnDDL('t', 'active', { type: 'boolean', default: false });
1036
+ expect(ddl).toContain('DEFAULT FALSE');
1037
+ });
1038
+
1039
+ it('ends with semicolon', () => {
1040
+ const ddl = generatePgAddColumnDDL('t', 'col', { type: 'string' });
1041
+ expect(ddl.trim()).toMatch(/;$/);
1042
+ });
1043
+ });
1044
+
1045
+ // ─── P. generatePgIndexDDL ──────────────────────────────────────────────────
1046
+
1047
+ describe('generatePgIndexDDL', () => {
1048
+ it('single-field index', () => {
1049
+ const ddls = generatePgIndexDDL('posts', [{ fields: ['status'] }]);
1050
+ expect(ddls).toHaveLength(1);
1051
+ expect(ddls[0]).toContain('CREATE INDEX IF NOT EXISTS');
1052
+ expect(ddls[0]).toContain('"posts"("status")');
1053
+ });
1054
+
1055
+ it('unique index', () => {
1056
+ const ddls = generatePgIndexDDL('posts', [{ fields: ['slug'], unique: true }]);
1057
+ expect(ddls[0]).toContain('CREATE UNIQUE INDEX');
1058
+ });
1059
+
1060
+ it('composite index', () => {
1061
+ const ddls = generatePgIndexDDL('posts', [{ fields: ['userId', 'createdAt'] }]);
1062
+ expect(ddls[0]).toContain('"userId", "createdAt"');
1063
+ });
1064
+
1065
+ it('index name derived from table + fields', () => {
1066
+ const ddls = generatePgIndexDDL('posts', [{ fields: ['status'] }]);
1067
+ expect(ddls[0]).toContain('idx_posts_status');
1068
+ });
1069
+
1070
+ it('multiple indexes', () => {
1071
+ const ddls = generatePgIndexDDL('t', [
1072
+ { fields: ['a'] },
1073
+ { fields: ['b', 'c'] },
1074
+ ]);
1075
+ expect(ddls).toHaveLength(2);
1076
+ });
1077
+
1078
+ it('empty indexes → empty array', () => {
1079
+ const ddls = generatePgIndexDDL('t', []);
1080
+ expect(ddls).toEqual([]);
1081
+ });
1082
+ });
1083
+
1084
+ // ─── Q. generatePgFTSDDL ────────────────────────────────────────────────────
1085
+
1086
+ describe('generatePgFTSDDL', () => {
1087
+ it('returns 5 DDL statements', () => {
1088
+ const ddls = generatePgFTSDDL('posts', ['title', 'body']);
1089
+ expect(ddls).toHaveLength(5);
1090
+ });
1091
+
1092
+ it('step 1: ALTER TABLE ADD COLUMN _fts tsvector', () => {
1093
+ const ddls = generatePgFTSDDL('posts', ['title']);
1094
+ expect(ddls[0]).toContain('ALTER TABLE "posts" ADD COLUMN IF NOT EXISTS "_fts" tsvector');
1095
+ });
1096
+
1097
+ it('step 2: GIN index on _fts column', () => {
1098
+ const ddls = generatePgFTSDDL('posts', ['title']);
1099
+ expect(ddls[1]).toContain('CREATE INDEX IF NOT EXISTS');
1100
+ expect(ddls[1]).toContain('USING gin');
1101
+ expect(ddls[1]).toContain('"_fts"');
1102
+ });
1103
+
1104
+ it('step 2: GIN index name includes _fts suffix', () => {
1105
+ const ddls = generatePgFTSDDL('posts', ['title']);
1106
+ expect(ddls[1]).toContain('"idx_posts_fts"');
1107
+ });
1108
+
1109
+ it('step 3: trigger function with to_tsvector', () => {
1110
+ const ddls = generatePgFTSDDL('posts', ['title', 'body']);
1111
+ expect(ddls[2]).toContain('CREATE OR REPLACE FUNCTION');
1112
+ expect(ddls[2]).toContain('"posts_fts_trigger"');
1113
+ expect(ddls[2]).toContain("to_tsvector('simple'");
1114
+ expect(ddls[2]).toContain('RETURNS trigger');
1115
+ expect(ddls[2]).toContain('plpgsql');
1116
+ });
1117
+
1118
+ it('step 3: trigger function coalesces all fields', () => {
1119
+ const ddls = generatePgFTSDDL('posts', ['title', 'body']);
1120
+ expect(ddls[2]).toContain('coalesce(NEW."title", \'\')');
1121
+ expect(ddls[2]).toContain('coalesce(NEW."body", \'\')');
1122
+ });
1123
+
1124
+ it('step 4: BEFORE INSERT OR UPDATE trigger', () => {
1125
+ const ddls = generatePgFTSDDL('posts', ['title']);
1126
+ expect(ddls[3]).toContain('DROP TRIGGER IF EXISTS');
1127
+ expect(ddls[3]).toContain('CREATE TRIGGER');
1128
+ expect(ddls[3]).toContain('BEFORE INSERT OR UPDATE');
1129
+ expect(ddls[3]).toContain('FOR EACH ROW');
1130
+ expect(ddls[3]).toContain('EXECUTE FUNCTION');
1131
+ });
1132
+
1133
+ it('step 4: trigger name is tableName_fts_update', () => {
1134
+ const ddls = generatePgFTSDDL('posts', ['title']);
1135
+ expect(ddls[3]).toContain('"posts_fts_update"');
1136
+ });
1137
+
1138
+ it('step 5: backfill UPDATE with bare coalesce', () => {
1139
+ const ddls = generatePgFTSDDL('posts', ['title', 'body']);
1140
+ expect(ddls[4]).toContain('UPDATE "posts" SET "_fts"');
1141
+ expect(ddls[4]).toContain("to_tsvector('simple'");
1142
+ expect(ddls[4]).toContain('coalesce("title", \'\')');
1143
+ expect(ddls[4]).toContain('coalesce("body", \'\')');
1144
+ // Backfill should NOT have NEW. prefix
1145
+ expect(ddls[4]).not.toContain('NEW.');
1146
+ });
1147
+
1148
+ it('single field FTS', () => {
1149
+ const ddls = generatePgFTSDDL('t', ['body']);
1150
+ expect(ddls[2]).toContain('coalesce(NEW."body", \'\')');
1151
+ // No concatenation with || when single field
1152
+ expect(ddls[2]).not.toContain("|| ' ' ||");
1153
+ });
1154
+
1155
+ it('multiple fields joined with space separator', () => {
1156
+ const ddls = generatePgFTSDDL('t', ['a', 'b', 'c']);
1157
+ // Trigger function should concatenate with || ' ' ||
1158
+ expect(ddls[2]).toContain("|| ' ' ||");
1159
+ });
1160
+ });
1161
+
1162
+ // ─── R. generatePgTableDDL ──────────────────────────────────────────────────
1163
+
1164
+ describe('generatePgTableDDL', () => {
1165
+ it('returns array with CREATE TABLE', () => {
1166
+ const ddls = generatePgTableDDL('posts', { schema: {} } as TableConfig);
1167
+ expect(ddls.length).toBeGreaterThanOrEqual(1);
1168
+ expect(ddls[0]).toContain('CREATE TABLE');
1169
+ });
1170
+
1171
+ it('with indexes → includes CREATE INDEX', () => {
1172
+ const ddls = generatePgTableDDL('posts', {
1173
+ schema: {},
1174
+ indexes: [{ fields: ['status'] }],
1175
+ } as TableConfig);
1176
+ expect(ddls.some(d => d.includes('CREATE INDEX'))).toBe(true);
1177
+ });
1178
+
1179
+ it('with FTS → includes tsvector + GIN + trigger', () => {
1180
+ const ddls = generatePgTableDDL('posts', {
1181
+ schema: { title: { type: 'string' } },
1182
+ fts: ['title'],
1183
+ } as TableConfig);
1184
+ // 1 CREATE TABLE + 5 FTS DDLs = 6
1185
+ expect(ddls).toHaveLength(6);
1186
+ expect(ddls.some(d => d.includes('tsvector'))).toBe(true);
1187
+ expect(ddls.some(d => d.includes('gin'))).toBe(true);
1188
+ expect(ddls.some(d => d.includes('TRIGGER'))).toBe(true);
1189
+ });
1190
+
1191
+ it('no FTS → no tsvector', () => {
1192
+ const ddls = generatePgTableDDL('posts', { schema: {} } as TableConfig);
1193
+ expect(ddls.every(d => !d.includes('tsvector'))).toBe(true);
1194
+ });
1195
+
1196
+ it('no indexes, no fts → only CREATE TABLE', () => {
1197
+ const ddls = generatePgTableDDL('simple', { schema: {} } as TableConfig);
1198
+ expect(ddls).toHaveLength(1);
1199
+ expect(ddls[0]).toContain('CREATE TABLE');
1200
+ });
1201
+
1202
+ it('indexes + fts combined', () => {
1203
+ const ddls = generatePgTableDDL('full', {
1204
+ schema: { title: { type: 'string' }, status: { type: 'string' } },
1205
+ indexes: [{ fields: ['status'] }],
1206
+ fts: ['title'],
1207
+ } as TableConfig);
1208
+ // 1 CREATE TABLE + 1 INDEX + 5 FTS = 7
1209
+ expect(ddls).toHaveLength(7);
1210
+ });
1211
+
1212
+ it('empty indexes array → no CREATE INDEX', () => {
1213
+ const ddls = generatePgTableDDL('t', {
1214
+ schema: {},
1215
+ indexes: [],
1216
+ } as TableConfig);
1217
+ expect(ddls.every(d => !d.includes('CREATE INDEX'))).toBe(true);
1218
+ });
1219
+
1220
+ it('empty fts array → no FTS', () => {
1221
+ const ddls = generatePgTableDDL('t', {
1222
+ schema: {},
1223
+ fts: [],
1224
+ } as TableConfig);
1225
+ expect(ddls.every(d => !d.includes('tsvector'))).toBe(true);
1226
+ });
1227
+ });
1228
+
1229
+ // ─── S. PostgreSQL vs SQLite type differences ───────────────────────────────
1230
+
1231
+ describe('PostgreSQL vs SQLite type divergence', () => {
1232
+ it('boolean: PG uses BOOLEAN, SQLite uses INTEGER', () => {
1233
+ const pgDDL = generatePgCreateTableDDL('t', { schema: { flag: { type: 'boolean' } } } as TableConfig);
1234
+ const sqliteDDL = generateCreateTableDDL('t', { schema: { flag: { type: 'boolean' } } } as TableConfig);
1235
+ expect(pgDDL).toContain('BOOLEAN');
1236
+ expect(sqliteDDL).toContain('INTEGER');
1237
+ });
1238
+
1239
+ it('number: PG uses DOUBLE PRECISION, SQLite uses REAL', () => {
1240
+ const pgDDL = generatePgCreateTableDDL('t', { schema: { val: { type: 'number' } } } as TableConfig);
1241
+ const sqliteDDL = generateCreateTableDDL('t', { schema: { val: { type: 'number' } } } as TableConfig);
1242
+ expect(pgDDL).toContain('DOUBLE PRECISION');
1243
+ expect(sqliteDDL).toContain('REAL');
1244
+ });
1245
+
1246
+ it('datetime: PG uses TIMESTAMPTZ, SQLite uses TEXT', () => {
1247
+ const pgDDL = generatePgCreateTableDDL('t', { schema: { ts: { type: 'datetime' } } } as TableConfig);
1248
+ const sqliteDDL = generateCreateTableDDL('t', { schema: { ts: { type: 'datetime' } } } as TableConfig);
1249
+ expect(pgDDL).toContain('TIMESTAMPTZ');
1250
+ expect(sqliteDDL).toContain('"ts" TEXT');
1251
+ });
1252
+
1253
+ it('json: PG uses JSONB, SQLite uses TEXT', () => {
1254
+ const pgDDL = generatePgCreateTableDDL('t', { schema: { data: { type: 'json' } } } as TableConfig);
1255
+ const sqliteDDL = generateCreateTableDDL('t', { schema: { data: { type: 'json' } } } as TableConfig);
1256
+ expect(pgDDL).toContain('JSONB');
1257
+ expect(sqliteDDL).toContain('"data" TEXT');
1258
+ });
1259
+
1260
+ it('boolean default: PG uses TRUE/FALSE, SQLite uses 1/0', () => {
1261
+ const pgDDL = generatePgCreateTableDDL('t', { schema: { flag: { type: 'boolean', default: true } } } as TableConfig);
1262
+ const sqliteDDL = generateCreateTableDDL('t', { schema: { flag: { type: 'boolean', default: true } } } as TableConfig);
1263
+ expect(pgDDL).toContain('DEFAULT TRUE');
1264
+ expect(sqliteDDL).toContain('DEFAULT 1');
1265
+ });
1266
+
1267
+ it('string type is TEXT for both', () => {
1268
+ const pgDDL = generatePgCreateTableDDL('t', { schema: { name: { type: 'string' } } } as TableConfig);
1269
+ const sqliteDDL = generateCreateTableDDL('t', { schema: { name: { type: 'string' } } } as TableConfig);
1270
+ expect(pgDDL).toContain('"name" TEXT');
1271
+ expect(sqliteDDL).toContain('"name" TEXT');
1272
+ });
1273
+ });