@nodefony/drizzle 10.0.0-alpha.9 → 10.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +28 -26
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +5 -5
  5. package/dist/nodefony/command/orm-generate.js +3 -3
  6. package/dist/nodefony/config/defineModuleConfig.js +2 -2
  7. package/dist/nodefony/entity/userTable.js +1 -1
  8. package/dist/nodefony/registerStores.js +2 -2
  9. package/dist/nodefony/service/DrizzleService.js +26 -12
  10. package/dist/nodefony/src/DrizzleAuditStore.js +1 -1
  11. package/dist/nodefony/src/connectorTarget.js +1 -1
  12. package/dist/nodefony/src/migrator/DrizzleMigrator.js +3 -3
  13. package/dist/nodefony/src/migrator/adopt.js +1 -1
  14. package/dist/nodefony/src/migrator/destructive.js +2 -3
  15. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +1 -1
  16. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +1 -1
  17. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +6 -3
  18. package/dist/nodefony/src/migrator/explain.js +7 -10
  19. package/dist/nodefony/src/migrator/history.js +25 -2
  20. package/dist/nodefony/src/migrator/kit.js +23 -9
  21. package/dist/nodefony/src/migrator/rebuildCopy.js +6 -6
  22. package/dist/nodefony/src/migrator/resolve.js +4 -4
  23. package/dist/nodefony/src/migrator/sources.js +13 -8
  24. package/dist/nodefony/src/orm-core/DrizzleOrm.js +55 -32
  25. package/dist/nodefony/src/orm-core/DrizzleRepository.js +142 -40
  26. package/dist/nodefony/src/queryKit.js +5 -5
  27. package/dist/nodefony/src/sqliteJournal.js +54 -0
  28. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +1 -1
  29. package/dist/types/nodefony/src/connectorTarget.d.ts +1 -1
  30. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +4 -4
  31. package/dist/types/nodefony/src/migrator/destructive.d.ts +1 -1
  32. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +2 -2
  33. package/dist/types/nodefony/src/migrator/explain.d.ts +9 -9
  34. package/dist/types/nodefony/src/migrator/name.d.ts +1 -1
  35. package/dist/types/nodefony/src/migrator/types.d.ts +2 -2
  36. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +3 -3
  37. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +1 -1
  38. package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +2 -2
  39. package/dist/types/nodefony/src/queryKit.d.ts +9 -9
  40. package/dist/types/nodefony/src/safeTarget.d.ts +2 -2
  41. package/dist/types/nodefony/src/sqliteJournal.d.ts +53 -0
  42. package/docs/index.md +50 -39
  43. package/docs/migrations.md +1 -1
  44. package/package.json +16 -16
@@ -71,7 +71,8 @@ async function readJournal(dir, source) {
71
71
  }
72
72
  let journal;
73
73
  try {
74
- journal = JSON.parse(raw);
74
+ const parsed = JSON.parse(raw);
75
+ journal = typeof parsed === "object" && parsed !== null ? parsed : {};
75
76
  } catch (e) {
76
77
  throw new MigrationVerdictError({
77
78
  code: "NF_MIGRATE_UNKNOWN_FORMAT",
@@ -91,21 +92,25 @@ async function readJournal(dir, source) {
91
92
  facts: { file },
92
93
  nextActions: []
93
94
  }, `Le journal des migrations « ${file} » ne porte pas de liste « entries » : ce n'est pas un journal que ce format sait lire.`);
94
- if (!SUPPORTED_JOURNAL_VERSIONS.includes(String(journal.version))) throw new MigrationVerdictError({
95
+ const version = String(journal.version);
96
+ if (!SUPPORTED_JOURNAL_VERSIONS.includes(version)) throw new MigrationVerdictError({
95
97
  code: "NF_MIGRATE_UNKNOWN_FORMAT",
96
98
  connector: "",
97
99
  source,
98
100
  facts: {
99
101
  file,
100
- journalVersion: String(journal.version),
102
+ journalVersion: version,
101
103
  supported: SUPPORTED_JOURNAL_VERSIONS
102
104
  },
103
105
  nextActions: [{
104
106
  command: "npm update @nodefony/drizzle",
105
107
  args: ["update", "@nodefony/drizzle"]
106
108
  }]
107
- }, `Le journal de migrations de la source « ${source} » est en version ${String(journal.version)}, que cette version du framework ne sait pas lire (reconnue : ${SUPPORTED_JOURNAL_VERSIONS.join(", ")}).`);
108
- return journal;
109
+ }, `Le journal de migrations de la source « ${source} » est en version ${version}, que cette version du framework ne sait pas lire (reconnue : ${SUPPORTED_JOURNAL_VERSIONS.join(", ")}).`);
110
+ return {
111
+ version,
112
+ entries: journal.entries
113
+ };
109
114
  }
110
115
  /**
111
116
  * Lit un fichier de migration, valide son format et découpe ses statements.
@@ -370,8 +375,8 @@ function scanLine(line, needle, grammar, curseur) {
370
375
  */
371
376
  function delimiteurDollar(line, debut) {
372
377
  let i = debut + 1;
373
- while (i < line.length && /[A-Za-z0-9_]/.test(line[i])) {
374
- if (i === debut + 1 && /[0-9]/.test(line[i])) return null;
378
+ while (i < line.length && /[A-Za-z0-9_]/.test(line.charAt(i))) {
379
+ if (i === debut + 1 && /[0-9]/.test(line.charAt(i))) return null;
375
380
  i += 1;
376
381
  }
377
382
  return line[i] === "$" ? line.slice(debut, i + 1) : null;
@@ -390,7 +395,7 @@ function delimiteurDollar(line, debut) {
390
395
  function createdTables(files) {
391
396
  const found = /* @__PURE__ */ new Set();
392
397
  const pattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z0-9_$]+)[`"']?/gi;
393
- for (const file of files) for (const statement of file.statements) for (const match of statement.matchAll(pattern)) found.add(match[1]);
398
+ for (const file of files) for (const statement of file.statements) for (const [, table] of statement.matchAll(pattern)) if (table !== void 0) found.add(table);
394
399
  return [...found];
395
400
  }
396
401
  /**
@@ -1,5 +1,6 @@
1
1
  import { schemaReader, toDollarParams } from "../migrator/catalog.js";
2
2
  import "../memoryDatabase.js";
3
+ import { enableWriteAheadLog } from "../sqliteJournal.js";
3
4
  import { notConfigured } from "../migrator/refusals.js";
4
5
  import { additiveSql, compareSchema, hasGap } from "../migrator/schemaDiff.js";
5
6
  import { describeTargetSafely } from "../safeTarget.js";
@@ -431,17 +432,18 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
431
432
  }
432
433
  async onConnect() {
433
434
  await this.#releasePrevious();
434
- this.#tables = Object.create(null);
435
+ const tables = Object.create(null);
436
+ this.#tables = tables;
435
437
  this.#relations = Object.create(null);
436
438
  const entities = this.#ownEntities();
437
439
  switch (this.#dialect) {
438
440
  case "sqlite":
439
- await this.#connectSqlite(entities);
441
+ await this.#connectSqlite(entities, tables);
440
442
  break;
441
443
  case "postgres":
442
- await this.#connectPostgres(entities);
444
+ await this.#connectPostgres(entities, tables);
443
445
  break;
444
- case "mysql": await this.#connectMysql(entities);
446
+ case "mysql": await this.#connectMysql(entities, tables);
445
447
  }
446
448
  if (this.#deriveSchema) {
447
449
  await this.#reconcileSchema();
@@ -461,7 +463,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
461
463
  * La méthode devient asynchrone pour cette seule raison ; le pilote, lui,
462
464
  * reste synchrone une fois chargé (`client.exec`, `client.pragma`).
463
465
  */
464
- async #connectSqlite(entities) {
466
+ async #connectSqlite(entities, tables) {
465
467
  let Sqlite;
466
468
  let sqliteDrizzle;
467
469
  try {
@@ -471,12 +473,14 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
471
473
  Sqlite = resolved;
472
474
  sqliteDrizzle = (await import("drizzle-orm/better-sqlite3")).drizzle;
473
475
  } catch (e) {
474
- throw new Error(`DrizzleOrm "${this.name}": the sqlite dialect needs the optional driver \`better-sqlite3\` (run \`npm i better-sqlite3\`). ${e.message}`, { cause: e });
476
+ throw new Error(`DrizzleOrm "${this.name}": the sqlite dialect needs the optional driver \`better-sqlite3\` (run \`npm i better-sqlite3\`). ` + e.message, { cause: e });
475
477
  }
476
478
  const client = new Sqlite(this.#filename);
477
- if (this.#filename !== ":memory:") {
478
- client.pragma("journal_mode = WAL");
479
- client.pragma("synchronous = NORMAL");
479
+ if (this.#filename !== ":memory:") try {
480
+ await enableWriteAheadLog(client);
481
+ } catch (e) {
482
+ client.close();
483
+ throw e;
480
484
  }
481
485
  client.pragma("foreign_keys = ON");
482
486
  this.#client = client;
@@ -506,7 +510,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
506
510
  for (const entity of entities) {
507
511
  this.#assertDialectTable(entity, SQLiteTable, "sqlite");
508
512
  const table = entity.schema;
509
- this.#tables[entity.name] = table;
513
+ tables[entity.name] = table;
510
514
  entity.model = table;
511
515
  }
512
516
  if (!this.#deriveSchema) return;
@@ -537,12 +541,12 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
537
541
  #rawQuery = async (sql, params = []) => {
538
542
  switch (this.#dialect) {
539
543
  case "sqlite": {
540
- const statement = this.#client.prepare(sql);
544
+ const statement = this.#sqliteClient().prepare(sql);
541
545
  return statement.reader ? statement.all(...params) : [];
542
546
  }
543
- case "postgres": return (await this.#pgPool.query(toDollarParams(sql), params)).rows;
547
+ case "postgres": return (await this.#pgClient().query(toDollarParams(sql), params)).rows;
544
548
  case "mysql": {
545
- const [rows] = await this.#mysqlPool.query(sql, params);
549
+ const [rows] = await this.#mysqlClient().query(sql, params);
546
550
  return Array.isArray(rows) ? rows : [];
547
551
  }
548
552
  }
@@ -555,13 +559,13 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
555
559
  async #rawExec(sql) {
556
560
  switch (this.#dialect) {
557
561
  case "sqlite":
558
- this.#client.exec(sql);
562
+ this.#sqliteClient().exec(sql);
559
563
  return;
560
564
  case "postgres":
561
- await this.#pgPool.query(sql);
565
+ await this.#pgClient().query(sql);
562
566
  return;
563
567
  case "mysql":
564
- await this.#mysqlPool.query(sql);
568
+ await this.#mysqlClient().query(sql);
565
569
  return;
566
570
  }
567
571
  }
@@ -640,20 +644,20 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
640
644
  case "sqlite": {
641
645
  const t = table;
642
646
  if (empechees.has(getTableConfig(t).name)) continue;
643
- for (const statement of this.#createIndexesSQL(t)) this.#client.exec(statement);
647
+ for (const statement of this.#createIndexesSQL(t)) this.#sqliteClient().exec(statement);
644
648
  break;
645
649
  }
646
650
  case "postgres": {
647
651
  const t = table;
648
652
  if (empechees.has(getTableConfig$1(t).name)) continue;
649
- for (const statement of this.#createIndexesPgSQL(t)) await this.#pgPool.query(statement);
653
+ for (const statement of this.#createIndexesPgSQL(t)) await this.#pgClient().query(statement);
650
654
  break;
651
655
  }
652
656
  case "mysql": {
653
657
  const t = table;
654
658
  if (empechees.has(getTableConfig$2(t).name)) continue;
655
659
  for (const statement of this.#createIndexesMysqlSQL(t)) try {
656
- await this.#mysqlPool.query(statement);
660
+ await this.#mysqlClient().query(statement);
657
661
  } catch (error) {
658
662
  if (error.code !== "ER_DUP_KEYNAME") throw error;
659
663
  }
@@ -677,7 +681,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
677
681
  * déploiement SQLite ne les tire jamais. Pool partagé ; DDL dérivé pour dev/test
678
682
  * (prod = drizzle-kit). Échec d'import → message actionnable (`npm i pg`).
679
683
  */
680
- async #connectPostgres(entities) {
684
+ async #connectPostgres(entities, tables) {
681
685
  if (!this.#url) throw new Error(`DrizzleOrm "${this.name}": dialect "postgres" requires a connection \`url\`.`);
682
686
  let PoolCtor;
683
687
  let pgDrizzle;
@@ -708,7 +712,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
708
712
  this.#pgPool = pool;
709
713
  this.#db = pgDrizzle(pool);
710
714
  try {
711
- await this.#finishPostgres(entities, pool, pgDrizzle);
715
+ await this.#finishPostgres(entities, tables, pool, pgDrizzle);
712
716
  } catch (e) {
713
717
  this.#unwireAll();
714
718
  await pool.end().catch(() => void 0);
@@ -719,11 +723,11 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
719
723
  }
720
724
  }
721
725
  /** Suite de la connexion postgres — isolée pour rendre `connect()` atomique. */
722
- async #finishPostgres(entities, pool, pgDrizzle) {
726
+ async #finishPostgres(entities, tables, pool, pgDrizzle) {
723
727
  this.#beginTx = async () => {
724
728
  const cx = await pool.connect();
725
729
  const puitsTx = (err) => {
726
- this.connectionLost(`pg (transaction) : ${err?.message ?? String(err)}`);
730
+ this.connectionLost(`pg (transaction) : ${err instanceof Error ? err.message : String(err)}`);
727
731
  };
728
732
  cx.on("error", puitsTx);
729
733
  /** Rend la connexion, en retirant d'abord NOTRE puits. */
@@ -748,7 +752,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
748
752
  for (const entity of entities) {
749
753
  this.#assertDialectTable(entity, PgTable, "postgres");
750
754
  const table = entity.schema;
751
- this.#tables[entity.name] = table;
755
+ tables[entity.name] = table;
752
756
  entity.model = table;
753
757
  }
754
758
  if (!this.#deriveSchema) return;
@@ -809,7 +813,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
809
813
  */
810
814
  #wirePgLifecycle(pool) {
811
815
  const onError = (err) => {
812
- this.connectionLost(`pg: ${err?.message ?? String(err)}`);
816
+ this.connectionLost(`pg: ${err instanceof Error ? err.message : String(err)}`);
813
817
  };
814
818
  const onConnect = () => {
815
819
  this.connectionRestored();
@@ -853,7 +857,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
853
857
  this.connectionRestored();
854
858
  cx.on("error", (err) => {
855
859
  if (this.#mysqlPool !== pool) return;
856
- this.connectionLost(`mysql: ${err?.message ?? String(err)}`);
860
+ this.connectionLost(`mysql: ${err instanceof Error ? err.message : String(err)}`);
857
861
  });
858
862
  cx.stream?.once("close", () => {
859
863
  if (this.#mysqlPool !== pool) return;
@@ -873,7 +877,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
873
877
  * `datetime(3)` (kind `dateMs` du colKit) sont écrites/relues en UTC — mêmes
874
878
  * instants que `timestamptz` PG, sans dépendre de la timezone du serveur.
875
879
  */
876
- async #connectMysql(entities) {
880
+ async #connectMysql(entities, tables) {
877
881
  if (!this.#url) throw new Error(`DrizzleOrm "${this.name}": dialect "mysql" requires a connection \`url\`.`);
878
882
  let pool;
879
883
  let mysqlDrizzle;
@@ -903,7 +907,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
903
907
  this.#mysqlPool = pool;
904
908
  this.#db = mysqlDrizzle(pool);
905
909
  try {
906
- await this.#finishMysql(entities, pool, mysqlDrizzle);
910
+ await this.#finishMysql(entities, tables, pool, mysqlDrizzle);
907
911
  } catch (e) {
908
912
  this.#unwireAll();
909
913
  await pool.end().catch(() => void 0);
@@ -914,7 +918,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
914
918
  }
915
919
  }
916
920
  /** Suite de la connexion mysql — isolée pour rendre `connect()` atomique. */
917
- async #finishMysql(entities, pool, mysqlDrizzle) {
921
+ async #finishMysql(entities, tables, pool, mysqlDrizzle) {
918
922
  this.#beginTx = async () => {
919
923
  const cx = await pool.getConnection();
920
924
  try {
@@ -937,7 +941,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
937
941
  for (const entity of entities) {
938
942
  this.#assertDialectTable(entity, MySqlTable, "mysql");
939
943
  const table = entity.schema;
940
- this.#tables[entity.name] = table;
944
+ tables[entity.name] = table;
941
945
  entity.model = table;
942
946
  }
943
947
  if (!this.#deriveSchema) return;
@@ -977,7 +981,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
977
981
  const table = this.#tables?.[name];
978
982
  if (!this.#db) throw new Error(`DrizzleOrm "${this.name}": non connecté — impossible de rendre un dépôt pour "${name}". Les tables ne sont résolues qu'à la connexion. Si ce run est une commande en ligne, elle n'a pas déclaré \`externalServices\` (CONSOLE_DATA_RUN_PROFILE).`);
979
983
  if (!table) throw new Error(`DrizzleOrm "${this.name}": no entity table registered under "${name}".`);
980
- if (this.#repositories === null) this.#repositories = Object.create(null);
984
+ this.#repositories ??= Object.create(null);
981
985
  let repository = this.#repositories[name];
982
986
  if (repository === void 0) {
983
987
  repository = new DrizzleRepository(this.#db, table, this.#relations?.[name] ?? {}, this.name, this.#dialect);
@@ -1010,6 +1014,25 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
1010
1014
  throw error;
1011
1015
  }
1012
1016
  }
1017
+ /**
1018
+ * Connexion SQLite courante — lève si le connecteur n'est pas ouvert. Les
1019
+ * appelants ne passent ici qu'une fois `onConnect` engagé : la levée ne sert
1020
+ * qu'à typer, et à dire la cause si l'ordre était un jour rompu.
1021
+ */
1022
+ #sqliteClient() {
1023
+ if (this.#client === null) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
1024
+ return this.#client;
1025
+ }
1026
+ /** Pool PostgreSQL courant — même contrat que {@link #sqliteClient}. */
1027
+ #pgClient() {
1028
+ if (this.#pgPool === null) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
1029
+ return this.#pgPool;
1030
+ }
1031
+ /** Pool MySQL courant — même contrat que {@link #sqliteClient}. */
1032
+ #mysqlClient() {
1033
+ if (this.#mysqlPool === null) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
1034
+ return this.#mysqlPool;
1035
+ }
1013
1036
  getNativeConnection() {
1014
1037
  if (!this.#db) throw new Error(`DrizzleOrm "${this.name}": not connected.`);
1015
1038
  return this.#db;
@@ -1123,7 +1146,7 @@ var DrizzleOrm = class DrizzleOrm extends Orm {
1123
1146
  * @returns sonde `pool` (taille max, idle, empruntées, en attente).
1124
1147
  */
1125
1148
  #probePgPool(pool) {
1126
- const max = pool.options?.max;
1149
+ const max = pool.options.max;
1127
1150
  return { pool: {
1128
1151
  size: typeof max === "number" ? max : 10,
1129
1152
  available: pool.idleCount,
@@ -24,6 +24,25 @@ const PREPARED_CACHE_MAX = 128;
24
24
  /** Séquence des noms de prepared statements (PG les exige uniques par process). */
25
25
  let preparedNameSeq = 0;
26
26
  /**
27
+ * Les comparaisons scalaires, dans l'ordre où elles entrent dans le `WHERE`.
28
+ *
29
+ * UNE table pour le chemin direct (`#pushOperators`) ET le chemin préparé
30
+ * (`#buildPreparedSelect`) : deux listes recopiées finiraient par traduire le
31
+ * même critère en deux SQL différents, selon que la forme est en cache ou non.
32
+ * Leur texte SQL ne dépend que de l'opérateur, jamais de la valeur — c'est ce
33
+ * qui les rend préparables (≠ `$in`/`$nin`, dont le SQL suit la cardinalité).
34
+ */
35
+ const COMPARISONS = [
36
+ ["$eq", eq],
37
+ ["$ne", ne],
38
+ ["$gt", gt],
39
+ ["$gte", gte],
40
+ ["$lt", lt],
41
+ ["$lte", lte]
42
+ ];
43
+ /** Opérateurs qu'une forme préparée sait rendre : les comparaisons et `$null`. */
44
+ const PREPARABLE_OPERATORS = /* @__PURE__ */ new Set([...COMPARISONS.map(([op]) => op), "$null"]);
45
+ /**
27
46
  * Repository portable (contrat {@link IRepository}) au-dessus d'une table Drizzle
28
47
  * + driver `better-sqlite3`.
29
48
  *
@@ -83,9 +102,35 @@ var DrizzleRepository = class DrizzleRepository {
83
102
  this.#dialect = dialect;
84
103
  this.#transactional = transactional;
85
104
  }
86
- /** Colonne Drizzle d'une table par nom logique. */
105
+ /**
106
+ * Colonne Drizzle d'une table par nom logique ; `undefined` si elle n'existe pas.
107
+ *
108
+ * 🔴 `Object.hasOwn` sur l'objet des COLONNES, jamais `table[name]` : le nom
109
+ * vient souvent de la requête (`?sort=`), et une clé héritée du prototype
110
+ * (`constructor`, `__proto__`, `toString`) ne vaut pas `undefined` — elle
111
+ * franchissait la liste blanche et cassait plus loin dans le pilote.
112
+ * `getTableColumns` rend l'objet tel quel, sans copie.
113
+ */
87
114
  #col(table, name) {
88
- return table[name];
115
+ const columns = getTableColumns(table);
116
+ return Object.hasOwn(columns, name) ? columns[name] : void 0;
117
+ }
118
+ /**
119
+ * Colonne EXIGÉE : un nom inconnu lève au lieu de partir dans le SQL.
120
+ *
121
+ * Strict (B2) : champ inconnu = erreur, pas un skip silencieux (qui ferait
122
+ * disparaître la condition → fuite « renvoie tout »), ni un `undefined` glissé
123
+ * dans un `ORDER BY` (rendu `no such column: asc`). Même contrat que Mongoose.
124
+ * Requêtes natives → `getNativeConnection()`.
125
+ *
126
+ * @param name - nom logique du champ (critère, tri, clé étrangère).
127
+ * @param table - table qui doit le porter (défaut : celle du repository).
128
+ * @throws UnknownCriteriaField si la table ne porte pas ce champ.
129
+ */
130
+ #requireCol(name, table = this.#table) {
131
+ const col = this.#col(table, name);
132
+ if (col === void 0) throw new UnknownCriteriaField(name, getTableName(table), Object.keys(getTableColumns(table)));
133
+ return col;
89
134
  }
90
135
  /**
91
136
  * Colonnes de la PK de la table (mémoïsées) : colonnes inline `.primaryKey()`
@@ -121,18 +166,49 @@ var DrizzleRepository = class DrizzleRepository {
121
166
  *
122
167
  * Fallback sans PK déclarée : `rowid` (SQLite-only — toutes les entités
123
168
  * framework ont une PK ; une table d'app sans PK est un cas sqlite assumé).
169
+ *
170
+ * Court-circuit : un critère qui fixe TOUTES les colonnes de la PK par une
171
+ * égalité désigne déjà au plus une ligne — le `WHERE` nu suffit, et la
172
+ * sous-requête n'est plus payée. Elle coûtait ~60 µs par UPDATE en SQLite
173
+ * (recompilation d'un SQL plus long + table dérivée matérialisée, mesuré par
174
+ * `scripts/micro/micro-update-pick.mjs` du skill `nodefony-load-test`), soit
175
+ * l'essentiel de l'écart du banc ORM face à NestJS (#510).
176
+ *
177
+ * @param criteria - critère portable de la mutation.
178
+ * @returns le prédicat qui borne la mutation à au plus une ligne.
124
179
  */
125
- #pickOne(where) {
180
+ #pickOne(criteria) {
181
+ const where = this.#where(criteria);
126
182
  const pk = this.#pkColumns();
183
+ if (where && pk && this.#pinsPrimaryKey(pk, criteria)) return where;
127
184
  if (!pk) return where ? sql`rowid in (select rowid from ${this.#table} where ${where} limit 1)` : sql`rowid in (select rowid from ${this.#table} limit 1)`;
128
185
  const qualified = sql.join(pk, sql.raw(", "));
129
186
  const inner = where ? sql`select ${qualified} from ${this.#table} where ${where} limit 1` : sql`select ${qualified} from ${this.#table} limit 1`;
130
187
  const output = sql.join(pk.map((col) => sql.identifier(col.name)), sql.raw(", "));
131
- const first = pk[0];
188
+ const first = pk.at(0);
132
189
  const target = pk.length === 1 && first ? sql`${first}` : sql`(${qualified})`;
133
190
  return sql`${target} in (select ${output} from (${inner}) as picked)`;
134
191
  }
135
192
  /**
193
+ * Vrai si le critère fixe chaque colonne de la PK par une égalité à une
194
+ * valeur (ni `null`, ni opérateur, ni branche de `$or`) — l'unicité de la
195
+ * PK garantit alors « au plus une ligne » sans sous-requête.
196
+ *
197
+ * @param pk - colonnes de la PK (au plus 31 : masque de bits).
198
+ * @param criteria - critère portable de la mutation.
199
+ */
200
+ #pinsPrimaryKey(pk, criteria) {
201
+ if (criteria === void 0 || pk.length > 31) return false;
202
+ let pinned = 0;
203
+ for (const [field, value] of Object.entries(criteria)) {
204
+ if (field === "$or" || value === null || value === void 0 || isFieldOperators(value)) continue;
205
+ const col = this.#col(this.#table, field);
206
+ const i = col === void 0 ? -1 : pk.indexOf(col);
207
+ if (i !== -1) pinned |= 1 << i;
208
+ }
209
+ return pinned === (1 << pk.length) - 1;
210
+ }
211
+ /**
136
212
  * Lignes affectées, normalisé par driver : better-sqlite3 `{changes}` /
137
213
  * pg `{rowCount}` / mysql2 tuple `[ResultSetHeader{affectedRows}, fields]`.
138
214
  */
@@ -211,12 +287,10 @@ var DrizzleRepository = class DrizzleRepository {
211
287
  }
212
288
  /** Empile les conditions d'un objet d'opérateurs riches sur une colonne. */
213
289
  #pushOperators(conds, col, ops) {
214
- if (ops.$eq !== void 0) conds.push(eq(col, ops.$eq));
215
- if (ops.$ne !== void 0) conds.push(ne(col, ops.$ne));
216
- if (ops.$gt !== void 0) conds.push(gt(col, ops.$gt));
217
- if (ops.$gte !== void 0) conds.push(gte(col, ops.$gte));
218
- if (ops.$lt !== void 0) conds.push(lt(col, ops.$lt));
219
- if (ops.$lte !== void 0) conds.push(lte(col, ops.$lte));
290
+ for (const [op, compare] of COMPARISONS) {
291
+ const value = ops[op];
292
+ if (value !== void 0) conds.push(compare(col, value));
293
+ }
220
294
  if (ops.$in !== void 0) conds.push(inArray(col, [...ops.$in]));
221
295
  if (ops.$nin !== void 0) conds.push(notInArray(col, [...ops.$nin]));
222
296
  if (ops.$like !== void 0) conds.push(likeCond(this.#dialect, sql`${col}`, ops.$like));
@@ -229,12 +303,17 @@ var DrizzleRepository = class DrizzleRepository {
229
303
  for (const [field, value] of Object.entries(criteria)) {
230
304
  if (field === "$or") {
231
305
  if (!Array.isArray(value)) throw new Error(`DrizzleRepository(${getTableName(this.#table)}): $or attend un tableau de critères.`);
232
- const branches = value.map((branch) => this.#where(branch)).filter((branch) => branch !== void 0);
233
- if (branches.length > 0) conds.push(branches.length === 1 ? branches[0] : or(...branches));
306
+ if (value.length === 0) {
307
+ conds.push(sql`1 = 0`);
308
+ continue;
309
+ }
310
+ const branches = value.map((branch) => this.#where(branch));
311
+ if (branches.some((branch) => branch === void 0)) continue;
312
+ const [first, ...rest] = branches;
313
+ if (first !== void 0) conds.push(rest.length === 0 ? first : or(first, ...rest));
234
314
  continue;
235
315
  }
236
- const col = this.#col(this.#table, field);
237
- if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
316
+ const col = this.#requireCol(field);
238
317
  if (isFieldOperators(value)) this.#pushOperators(conds, col, value);
239
318
  else if (value === null) conds.push(isNull(col));
240
319
  else conds.push(eq(col, value));
@@ -251,8 +330,14 @@ var DrizzleRepository = class DrizzleRepository {
251
330
  * présence de limit/offset. Les VALEURS n'en font jamais partie : elles
252
331
  * deviennent des placeholders (`p<i>`, `lim`, `off`) bindés à l'exécution.
253
332
  *
254
- * Repli (`null`) par construction : `$or` et opérateurs riches (le SQL varie
255
- * avec la cardinalité — `$in` — ou la combinaison d'opérateurs), valeur
333
+ * Les comparaisons scalaires ({@link COMPARISONS}) et `$null` entrent dans la
334
+ * forme : la COMBINAISON d'opérateurs d'un champ fait partie de la clé, leurs
335
+ * valeurs deviennent des placeholders `p<i>$<op>`. C'est le chemin des
336
+ * lectures bornées dans le temps (`expiresAt: { $gt: now }` — la révocation
337
+ * des jetons, lue à chaque requête authentifiée).
338
+ *
339
+ * Repli (`null`) par construction : `$or`, `$in`/`$nin` (le SQL suit la
340
+ * cardinalité), `$like` (clause d'échappement propre au dialecte), valeur
256
341
  * `undefined` (le chemin actuel la rejette, même contrat), transaction
257
342
  * (cf ctor), cache plein ({@link PREPARED_CACHE_MAX}).
258
343
  */
@@ -262,7 +347,22 @@ var DrizzleRepository = class DrizzleRepository {
262
347
  if (criteria) {
263
348
  let i = 0;
264
349
  for (const [field, value] of Object.entries(criteria)) {
265
- if (field === "$or" || value === void 0 || isFieldOperators(value)) return null;
350
+ if (field === "$or" || value === void 0) return null;
351
+ if (isFieldOperators(value)) {
352
+ let ops = "";
353
+ for (const op in value) if (!PREPARABLE_OPERATORS.has(op)) return null;
354
+ for (const [op] of COMPARISONS) {
355
+ const operand = value[op];
356
+ if (operand !== void 0) {
357
+ ops += op;
358
+ params[`p${i}${op}`] = operand;
359
+ }
360
+ }
361
+ if (value.$null !== void 0) ops += value.$null ? "$null1" : "$null0";
362
+ key += `${field}\x04${ops}\x00`;
363
+ i++;
364
+ continue;
365
+ }
266
366
  if (value === null) key += `${field}\x01\x00`;
267
367
  else {
268
368
  key += `${field}\x00`;
@@ -305,14 +405,19 @@ var DrizzleRepository = class DrizzleRepository {
305
405
  const conds = [];
306
406
  let i = 0;
307
407
  for (const [field, value] of Object.entries(criteria)) {
308
- const col = this.#col(this.#table, field);
309
- if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
408
+ const col = this.#requireCol(field);
409
+ if (isFieldOperators(value)) {
410
+ for (const [op, compare] of COMPARISONS) if (value[op] !== void 0) conds.push(compare(col, sql.param(sql.placeholder(`p${i}${op}`), col)));
411
+ if (value.$null !== void 0) conds.push(value.$null ? isNull(col) : isNotNull(col));
412
+ i++;
413
+ continue;
414
+ }
310
415
  conds.push(value === null ? isNull(col) : eq(col, sql.param(sql.placeholder(`p${i}`), col)));
311
416
  i++;
312
417
  }
313
418
  if (conds.length > 0) query = query.where(conds.length === 1 ? conds[0] : and(...conds));
314
419
  }
315
- if (options?.order?.length) query = query.orderBy(...options.order.map(([field, dir]) => dir === "DESC" ? desc(this.#col(this.#table, field)) : asc(this.#col(this.#table, field))));
420
+ if (options?.order?.length) query = query.orderBy(...options.order.map(([field, dir]) => dir === "DESC" ? desc(this.#requireCol(field)) : asc(this.#requireCol(field))));
316
421
  if (options?.limit !== void 0) query = query.limit(sql.placeholder("lim"));
317
422
  else if (options?.offset !== void 0 && this.#dialect === "sqlite") query = query.limit(sql`-1`);
318
423
  else if (options?.offset !== void 0 && this.#dialect === "mysql") query = query.limit(Number.MAX_SAFE_INTEGER);
@@ -335,7 +440,7 @@ var DrizzleRepository = class DrizzleRepository {
335
440
  const shape = this.#selectShape(criteria, options);
336
441
  if (shape === null) return null;
337
442
  let cache = this.#preparedSelects;
338
- if (cache === null) cache = this.#preparedSelects = /* @__PURE__ */ new Map();
443
+ cache ??= this.#preparedSelects = /* @__PURE__ */ new Map();
339
444
  let entry = cache.get(shape.key);
340
445
  if (entry === void 0) {
341
446
  if (cache.size >= PREPARED_CACHE_MAX) return null;
@@ -364,12 +469,12 @@ var DrizzleRepository = class DrizzleRepository {
364
469
  let query = this.#db.select().from(execTable(this.#table)).$dynamic();
365
470
  const where = this.#where(criteria);
366
471
  if (where) query = query.where(where);
367
- if (options?.order?.length) query = query.orderBy(...options.order.map(([field, dir]) => dir === "DESC" ? desc(this.#col(this.#table, field)) : asc(this.#col(this.#table, field))));
472
+ if (options?.order?.length) query = query.orderBy(...options.order.map(([field, dir]) => dir === "DESC" ? desc(this.#requireCol(field)) : asc(this.#requireCol(field))));
368
473
  if (options?.limit !== void 0) query = query.limit(options.limit);
369
474
  else if (options?.offset !== void 0 && this.#dialect === "sqlite") query = query.limit(sql`-1`);
370
475
  else if (options?.offset !== void 0 && this.#dialect === "mysql") query = query.limit(Number.MAX_SAFE_INTEGER);
371
476
  if (options?.offset !== void 0) query = query.offset(options.offset);
372
- return await this.#prof(query);
477
+ return this.#prof(query);
373
478
  }
374
479
  /** Eager-load manuel des relations déclarées (1 requête `IN (...)` par relation). */
375
480
  async #populate(rows, relations) {
@@ -379,7 +484,7 @@ var DrizzleRepository = class DrizzleRepository {
379
484
  if (!rel) throw new Error(`DrizzleRepository(${name}): relation "${name}" non déclarée.`);
380
485
  if (rel.type === "one-to-many") {
381
486
  const parentIds = rows.map((row) => row[rel.localKey]);
382
- const fkCol = this.#col(rel.targetTable, rel.foreignKey);
487
+ const fkCol = this.#requireCol(rel.foreignKey, rel.targetTable);
383
488
  const children = await this.#prof(this.#db.select().from(execTable(rel.targetTable)).where(inArray(fkCol, parentIds)));
384
489
  const byParent = /* @__PURE__ */ new Map();
385
490
  for (const child of children) {
@@ -391,7 +496,7 @@ var DrizzleRepository = class DrizzleRepository {
391
496
  for (const row of rows) row[name] = byParent.get(row[rel.localKey]) ?? [];
392
497
  } else {
393
498
  const fkValues = rows.map((row) => row[rel.foreignKey]).filter((value) => value !== null && value !== void 0);
394
- const idCol = this.#col(rel.targetTable, rel.targetKey);
499
+ const idCol = this.#requireCol(rel.targetKey, rel.targetTable);
395
500
  const parents = fkValues.length > 0 ? await this.#prof(this.#db.select().from(execTable(rel.targetTable)).where(inArray(idCol, fkValues))) : [];
396
501
  const byId = new Map(parents.map((p) => [p[rel.targetKey], p]));
397
502
  for (const row of rows) row[name] = byId.get(row[rel.foreignKey]) ?? null;
@@ -420,7 +525,7 @@ var DrizzleRepository = class DrizzleRepository {
420
525
  }
421
526
  async updateOne(criteria, data) {
422
527
  if (this.#dialect === "mysql") return this.#mysqlUpdateOneReturning(data, criteria);
423
- const pick = this.#pickOne(this.#where(criteria));
528
+ const pick = this.#pickOne(criteria);
424
529
  return (await this.#prof(this.#db.update(execTable(this.#table)).set(data).where(pick).returning()))[0] ?? null;
425
530
  }
426
531
  /**
@@ -441,8 +546,7 @@ var DrizzleRepository = class DrizzleRepository {
441
546
  values[field] = value;
442
547
  continue;
443
548
  }
444
- const col = this.#col(this.#table, field);
445
- if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
549
+ const col = this.#requireCol(field);
446
550
  const apply = (fn, v) => {
447
551
  set[field] = sql`${sql.raw(fn)}(${col}, ${v})`;
448
552
  values[field] = v;
@@ -457,9 +561,7 @@ var DrizzleRepository = class DrizzleRepository {
457
561
  }
458
562
  async upsert(criteria, update, insertOnly) {
459
563
  const target = Object.keys(criteria).map((field) => {
460
- const col = this.#col(this.#table, field);
461
- if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
462
- return col;
564
+ return this.#requireCol(field);
463
565
  });
464
566
  const write = this.#writeSet(update);
465
567
  const values = {
@@ -486,12 +588,11 @@ var DrizzleRepository = class DrizzleRepository {
486
588
  async increment(criteria, changes) {
487
589
  const setObj = {};
488
590
  for (const [field, delta] of Object.entries(changes)) {
489
- const col = this.#col(this.#table, field);
490
- if (!col) throw new UnknownCriteriaField(field, getTableName(this.#table), Object.keys(getTableColumns(this.#table)));
591
+ const col = this.#requireCol(field);
491
592
  setObj[field] = sql`${col} + ${delta}`;
492
593
  }
493
594
  if (this.#dialect === "mysql") return this.#mysqlUpdateOneReturning(setObj, criteria);
494
- const pick = this.#pickOne(this.#where(criteria));
595
+ const pick = this.#pickOne(criteria);
495
596
  return (await this.#prof(this.#db.update(execTable(this.#table)).set(setObj).where(pick).returning()))[0] ?? null;
496
597
  }
497
598
  async delete(criteria) {
@@ -510,8 +611,8 @@ var DrizzleRepository = class DrizzleRepository {
510
611
  }
511
612
  /** DELETE atomique d'AU PLUS une ligne (PK via `#pickOne`), RETURNING la ligne. */
512
613
  async #deleteOneReturning(criteria) {
513
- const pick = this.#pickOne(this.#where(criteria));
514
- return await this.#prof(this.#db.delete(execTable(this.#table)).where(pick).returning());
614
+ const pick = this.#pickOne(criteria);
615
+ return this.#prof(this.#db.delete(execTable(this.#table)).where(pick).returning());
515
616
  }
516
617
  /** PK obligatoire en mysql (les verbes re-SELECTent par PK). Fail-loud sinon. */
517
618
  #requirePk(verb) {
@@ -522,7 +623,8 @@ var DrizzleRepository = class DrizzleRepository {
522
623
  /** WHERE d'égalité sur la PK (valeurs plates lues par nom de colonne). */
523
624
  #pkWhere(pk, values) {
524
625
  const conds = pk.map((col) => eq(col, values[col.name]));
525
- return conds.length === 1 ? conds[0] : and(...conds);
626
+ const [first, ...rest] = conds;
627
+ return first !== void 0 && rest.length === 0 ? first : and(...conds);
526
628
  }
527
629
  /** SELECT d'UNE ligne par valeurs de PK (relecture post-mutation mysql). */
528
630
  async #selectByPk(pk, values) {
@@ -544,7 +646,7 @@ var DrizzleRepository = class DrizzleRepository {
544
646
  async #mysqlUpdateOneReturning(set, criteria) {
545
647
  const pk = this.#requirePk("updateOne");
546
648
  const where = this.#where(criteria);
547
- const target = (await this.#runSelect(criteria, { limit: 1 }))[0];
649
+ const target = (await this.#runSelect(criteria, { limit: 1 })).at(0);
548
650
  if (!target) return null;
549
651
  const pkCond = this.#pkWhere(pk, target);
550
652
  const result = await this.#prof(this.#db.update(execTable(this.#table)).set(set).where(where ? and(pkCond, where) : pkCond));
@@ -556,7 +658,7 @@ var DrizzleRepository = class DrizzleRepository {
556
658
  async #mysqlDeleteOneReturning(criteria) {
557
659
  const pk = this.#requirePk("deleteOne");
558
660
  const where = this.#where(criteria);
559
- const target = (await this.#runSelect(criteria, { limit: 1 }))[0];
661
+ const target = (await this.#runSelect(criteria, { limit: 1 })).at(0);
560
662
  if (!target) return null;
561
663
  const pkCond = this.#pkWhere(pk, target);
562
664
  const result = await this.#prof(this.#db.delete(execTable(this.#table)).where(where ? and(pkCond, where) : pkCond));
@@ -591,7 +693,7 @@ var DrizzleRepository = class DrizzleRepository {
591
693
  */
592
694
  async countDistinct(field, criteria) {
593
695
  const where = this.#where(criteria);
594
- const builder = this.#db.select({ value: countDistinct(this.#col(this.#table, field)) }).from(execTable(this.#table)).$dynamic();
696
+ const builder = this.#db.select({ value: countDistinct(this.#requireCol(field)) }).from(execTable(this.#table)).$dynamic();
595
697
  const rows = await this.#prof(where ? builder.where(where) : builder);
596
698
  return Number(rows[0]?.value ?? 0);
597
699
  }