@orkestrel/database 0.0.4 → 0.0.6
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.
- package/dist/src/browser/index.d.ts +13 -13
- package/dist/src/browser/index.js +26 -13
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +677 -525
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +11 -6
- package/dist/src/core/index.d.ts +11 -6
- package/dist/src/core/index.js +677 -525
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +178 -136
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +40 -19
- package/dist/src/server/index.d.ts +40 -19
- package/dist/src/server/index.js +178 -137
- package/dist/src/server/index.js.map +1 -1
- package/package.json +21 -18
package/dist/src/core/index.cjs
CHANGED
|
@@ -53,7 +53,7 @@ var DatabaseError = class extends Error {
|
|
|
53
53
|
super(message);
|
|
54
54
|
this.name = "DatabaseError";
|
|
55
55
|
this.code = code;
|
|
56
|
-
this.context = context;
|
|
56
|
+
if (context !== void 0) this.context = context;
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
/**
|
|
@@ -91,16 +91,7 @@ function isDatabaseError(value) {
|
|
|
91
91
|
* @returns `-1`, `0`, or `1`
|
|
92
92
|
*/
|
|
93
93
|
function compareValues(left, right) {
|
|
94
|
-
const
|
|
95
|
-
if (value === void 0) return 0;
|
|
96
|
-
if (value === null) return 1;
|
|
97
|
-
if (typeof value === "boolean") return 2;
|
|
98
|
-
if (typeof value === "number") return 3;
|
|
99
|
-
if (typeof value === "string") return 4;
|
|
100
|
-
return 5;
|
|
101
|
-
};
|
|
102
|
-
const leftRank = rankOf(left);
|
|
103
|
-
const rightRank = rankOf(right);
|
|
94
|
+
const [leftRank = 5, rightRank = 5] = [left, right].map((value) => value === void 0 ? 0 : value === null ? 1 : typeof value === "boolean" ? 2 : typeof value === "number" ? 3 : typeof value === "string" ? 4 : 5);
|
|
104
95
|
if (leftRank !== rightRank) return leftRank < rightRank ? -1 : 1;
|
|
105
96
|
if (typeof left === "number" && typeof right === "number") {
|
|
106
97
|
if (Number.isNaN(left) || Number.isNaN(right)) return Number.isNaN(left) ? Number.isNaN(right) ? 0 : 1 : -1;
|
|
@@ -460,19 +451,14 @@ function shapeToColumnType(shape) {
|
|
|
460
451
|
* ```
|
|
461
452
|
*/
|
|
462
453
|
function isDriverMeta(value) {
|
|
463
|
-
|
|
454
|
+
return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isFiniteNumber)(value.version) && (0, _orkestrel_contract.isArray)(value.schema) && value.schema.every((table) => (0, _orkestrel_contract.isRecord)(table) && (0, _orkestrel_contract.isString)(table.name) && (0, _orkestrel_contract.isString)(table.primary) && (0, _orkestrel_contract.isArray)(table.columns) && table.columns.every((column) => (0, _orkestrel_contract.isRecord)(column) && (0, _orkestrel_contract.isString)(column.name) && (0, _orkestrel_contract.isString)(column.type) && [
|
|
464
455
|
"text",
|
|
465
456
|
"integer",
|
|
466
457
|
"real",
|
|
467
458
|
"boolean",
|
|
468
459
|
"json",
|
|
469
460
|
"blob"
|
|
470
|
-
];
|
|
471
|
-
const isColumnType = (candidate) => (0, _orkestrel_contract.isString)(candidate) && COLUMN_TYPES.some((type) => type === candidate);
|
|
472
|
-
const isColumnSchema = (candidate) => (0, _orkestrel_contract.isRecord)(candidate) && (0, _orkestrel_contract.isString)(candidate.name) && isColumnType(candidate.type) && (0, _orkestrel_contract.isBoolean)(candidate.nullable);
|
|
473
|
-
const isIndexGroup = (candidate) => (0, _orkestrel_contract.isArray)(candidate) && candidate.every((entry) => (0, _orkestrel_contract.isString)(entry));
|
|
474
|
-
const isTableSchema = (candidate) => (0, _orkestrel_contract.isRecord)(candidate) && (0, _orkestrel_contract.isString)(candidate.name) && (0, _orkestrel_contract.isString)(candidate.primary) && (0, _orkestrel_contract.isArray)(candidate.columns) && candidate.columns.every(isColumnSchema) && (0, _orkestrel_contract.isArray)(candidate.indexes) && candidate.indexes.every(isIndexGroup);
|
|
475
|
-
return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isFiniteNumber)(value.version) && (0, _orkestrel_contract.isArray)(value.schema) && value.schema.every(isTableSchema);
|
|
461
|
+
].some((type) => type === column.type) && (0, _orkestrel_contract.isBoolean)(column.nullable)) && (0, _orkestrel_contract.isArray)(table.indexes) && table.indexes.every((index) => (0, _orkestrel_contract.isArray)(index) && index.every((column) => (0, _orkestrel_contract.isString)(column))));
|
|
476
462
|
}
|
|
477
463
|
/**
|
|
478
464
|
* Throw when an {@link ReadOptions.signal | AbortSignal} has fired — the shared
|
|
@@ -589,13 +575,12 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
589
575
|
}
|
|
590
576
|
});
|
|
591
577
|
}
|
|
592
|
-
|
|
593
|
-
for (const index of before.indexes) if (!table.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
|
|
578
|
+
for (const index of before.indexes) if (!table.indexes.some((candidate) => deepEqual(candidate, index))) steps.push({
|
|
594
579
|
operation: "index.remove",
|
|
595
580
|
table: table.name,
|
|
596
581
|
index
|
|
597
582
|
});
|
|
598
|
-
for (const index of table.indexes) if (!before.indexes.some((candidate) =>
|
|
583
|
+
for (const index of table.indexes) if (!before.indexes.some((candidate) => deepEqual(candidate, index))) steps.push({
|
|
599
584
|
operation: "index.add",
|
|
600
585
|
table: table.name,
|
|
601
586
|
index
|
|
@@ -735,529 +720,674 @@ async function* driverFindings(factory) {
|
|
|
735
720
|
indexes: []
|
|
736
721
|
};
|
|
737
722
|
const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
})
|
|
743
|
-
|
|
744
|
-
{
|
|
723
|
+
try {
|
|
724
|
+
const driver = factory();
|
|
725
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
726
|
+
await driver.close();
|
|
727
|
+
} catch (error) {
|
|
728
|
+
yield {
|
|
745
729
|
check: "open-close",
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
730
|
+
message: error instanceof Error ? error.message : String(error),
|
|
731
|
+
context: { error }
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const driver = factory();
|
|
736
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
737
|
+
const missing = await driver.read("users", "nope");
|
|
738
|
+
await driver.close();
|
|
739
|
+
if (missing !== void 0) yield {
|
|
740
|
+
check: "read-missing",
|
|
741
|
+
message: "read of a missing key must return undefined",
|
|
742
|
+
context: {
|
|
743
|
+
table: "users",
|
|
744
|
+
expected: void 0,
|
|
745
|
+
actual: missing
|
|
750
746
|
}
|
|
751
|
-
}
|
|
752
|
-
|
|
747
|
+
};
|
|
748
|
+
} catch (error) {
|
|
749
|
+
yield {
|
|
753
750
|
check: "read-missing",
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
751
|
+
message: error instanceof Error ? error.message : String(error),
|
|
752
|
+
context: { error }
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
writeRead: try {
|
|
756
|
+
const driver = factory();
|
|
757
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
758
|
+
const input = {
|
|
759
|
+
id: "u1",
|
|
760
|
+
name: "Ada",
|
|
761
|
+
age: 30,
|
|
762
|
+
meta: { tags: ["a"] }
|
|
763
|
+
};
|
|
764
|
+
await driver.write("users", "u1", input);
|
|
765
|
+
input.name = "Mutated after write";
|
|
766
|
+
if ((0, _orkestrel_contract.isRecord)(input.meta) && Array.isArray(input.meta.tags)) input.meta.tags.push("mutated");
|
|
767
|
+
const stored = await driver.read("users", "u1");
|
|
768
|
+
const original = {
|
|
769
|
+
id: "u1",
|
|
770
|
+
name: "Ada",
|
|
771
|
+
age: 30,
|
|
772
|
+
meta: { tags: ["a"] }
|
|
773
|
+
};
|
|
774
|
+
if (stored === void 0 || !deepEqual(stored, original)) {
|
|
775
|
+
await driver.close();
|
|
776
|
+
yield {
|
|
777
|
+
check: "copy-in",
|
|
778
|
+
message: "write must deep-copy the input row (including nested fields) rather than store it by reference",
|
|
779
|
+
context: {
|
|
760
780
|
table: "users",
|
|
761
|
-
expected:
|
|
762
|
-
actual:
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
check: "write-read",
|
|
768
|
-
run: async () => {
|
|
769
|
-
const driver = factory();
|
|
770
|
-
await driver.open(CONFORMANCE_SCHEMA);
|
|
771
|
-
const input = {
|
|
772
|
-
id: "u1",
|
|
773
|
-
name: "Ada",
|
|
774
|
-
age: 30,
|
|
775
|
-
meta: { tags: ["a"] }
|
|
776
|
-
};
|
|
777
|
-
await driver.write("users", "u1", input);
|
|
778
|
-
input.name = "Mutated after write";
|
|
779
|
-
if ((0, _orkestrel_contract.isRecord)(input.meta) && Array.isArray(input.meta.tags)) input.meta.tags.push("mutated");
|
|
780
|
-
const stored = await driver.read("users", "u1");
|
|
781
|
-
const original = {
|
|
782
|
-
id: "u1",
|
|
783
|
-
name: "Ada",
|
|
784
|
-
age: 30,
|
|
785
|
-
meta: { tags: ["a"] }
|
|
786
|
-
};
|
|
787
|
-
if (stored === void 0 || !deepEqual(stored, original)) {
|
|
788
|
-
await driver.close();
|
|
789
|
-
return findingOf("copy-in", "write must deep-copy the input row (including nested fields) rather than store it by reference", {
|
|
790
|
-
table: "users",
|
|
791
|
-
expected: original,
|
|
792
|
-
actual: stored
|
|
793
|
-
});
|
|
781
|
+
expected: original,
|
|
782
|
+
actual: stored
|
|
794
783
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
784
|
+
};
|
|
785
|
+
break writeRead;
|
|
786
|
+
}
|
|
787
|
+
stored.name = "Mutated after read";
|
|
788
|
+
if ((0, _orkestrel_contract.isRecord)(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
|
|
789
|
+
const reread = await driver.read("users", "u1");
|
|
790
|
+
if (reread === void 0 || !deepEqual(reread, original)) {
|
|
791
|
+
await driver.close();
|
|
792
|
+
yield {
|
|
793
|
+
check: "copy-out",
|
|
794
|
+
message: "read must deep-copy the stored row (including nested fields) rather than return it by reference",
|
|
795
|
+
context: {
|
|
796
|
+
table: "users",
|
|
797
|
+
expected: original,
|
|
798
|
+
actual: reread
|
|
805
799
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
800
|
+
};
|
|
801
|
+
break writeRead;
|
|
802
|
+
}
|
|
803
|
+
const overwrite = {
|
|
804
|
+
id: "u1",
|
|
805
|
+
name: "Ada Overwritten",
|
|
806
|
+
age: 31
|
|
807
|
+
};
|
|
808
|
+
await driver.write("users", "u1", overwrite);
|
|
809
|
+
const overwritten = await driver.read("users", "u1");
|
|
810
|
+
await driver.close();
|
|
811
|
+
if (overwritten === void 0 || !deepEqual(overwritten, overwrite)) yield {
|
|
812
|
+
check: "upsert",
|
|
813
|
+
message: "write must upsert-overwrite an existing key",
|
|
814
|
+
context: {
|
|
815
|
+
table: "users",
|
|
816
|
+
expected: overwrite,
|
|
817
|
+
actual: overwritten
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
} catch (error) {
|
|
821
|
+
yield {
|
|
822
|
+
check: "write-read",
|
|
823
|
+
message: error instanceof Error ? error.message : String(error),
|
|
824
|
+
context: { error }
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
deletePhase: try {
|
|
828
|
+
const driver = factory();
|
|
829
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
830
|
+
await driver.write("users", "u1", {
|
|
831
|
+
id: "u1",
|
|
832
|
+
name: "Ada",
|
|
833
|
+
age: 30
|
|
834
|
+
});
|
|
835
|
+
const first = await driver.delete("users", "u1");
|
|
836
|
+
if (first !== true) {
|
|
837
|
+
await driver.close();
|
|
838
|
+
yield {
|
|
839
|
+
check: "delete-true",
|
|
840
|
+
message: "delete of an existing key must return true",
|
|
841
|
+
context: {
|
|
815
842
|
table: "users",
|
|
816
|
-
expected:
|
|
817
|
-
actual:
|
|
818
|
-
}
|
|
843
|
+
expected: true,
|
|
844
|
+
actual: first
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
break deletePhase;
|
|
848
|
+
}
|
|
849
|
+
const second = await driver.delete("users", "u1");
|
|
850
|
+
await driver.close();
|
|
851
|
+
if (second !== false) yield {
|
|
852
|
+
check: "delete-false",
|
|
853
|
+
message: "delete of an already-removed key must return false",
|
|
854
|
+
context: {
|
|
855
|
+
table: "users",
|
|
856
|
+
expected: false,
|
|
857
|
+
actual: second
|
|
819
858
|
}
|
|
820
|
-
}
|
|
821
|
-
|
|
859
|
+
};
|
|
860
|
+
} catch (error) {
|
|
861
|
+
yield {
|
|
822
862
|
check: "delete",
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
actual: second
|
|
846
|
-
});
|
|
863
|
+
message: error instanceof Error ? error.message : String(error),
|
|
864
|
+
context: { error }
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
orderPhase: try {
|
|
868
|
+
const driver = factory();
|
|
869
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
870
|
+
for (const row of [
|
|
871
|
+
{
|
|
872
|
+
id: "c",
|
|
873
|
+
name: "C",
|
|
874
|
+
age: 3
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
id: "a",
|
|
878
|
+
name: "A",
|
|
879
|
+
age: 1
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
id: "b",
|
|
883
|
+
name: "B",
|
|
884
|
+
age: 2
|
|
847
885
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
id: "a",
|
|
862
|
-
name: "A",
|
|
863
|
-
age: 1
|
|
864
|
-
},
|
|
865
|
-
{
|
|
866
|
-
id: "b",
|
|
867
|
-
name: "B",
|
|
868
|
-
age: 2
|
|
869
|
-
}
|
|
870
|
-
]) await driver.write("users", row.id, row);
|
|
871
|
-
const expected = [
|
|
872
|
-
"a",
|
|
873
|
-
"b",
|
|
874
|
-
"c"
|
|
875
|
-
];
|
|
876
|
-
const keys = [...await driver.keys("users")];
|
|
877
|
-
if (!deepEqual(keys, expected)) {
|
|
878
|
-
await driver.close();
|
|
879
|
-
return findingOf("keys-order", "keys must be returned in ascending key order", {
|
|
880
|
-
table: "users",
|
|
881
|
-
expected,
|
|
882
|
-
actual: keys
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
const scanned = [];
|
|
886
|
-
for await (const row of driver.scan("users")) scanned.push(row);
|
|
887
|
-
const scannedIds = scanned.map((row) => row.id);
|
|
888
|
-
await driver.close();
|
|
889
|
-
if (!deepEqual(scannedIds, expected)) return findingOf("scan-order", "scan must yield rows in ascending key order", {
|
|
886
|
+
]) await driver.write("users", row.id, row);
|
|
887
|
+
const expected = [
|
|
888
|
+
"a",
|
|
889
|
+
"b",
|
|
890
|
+
"c"
|
|
891
|
+
];
|
|
892
|
+
const keys = [...await driver.keys("users")];
|
|
893
|
+
if (!deepEqual(keys, expected)) {
|
|
894
|
+
await driver.close();
|
|
895
|
+
yield {
|
|
896
|
+
check: "keys-order",
|
|
897
|
+
message: "keys must be returned in ascending key order",
|
|
898
|
+
context: {
|
|
890
899
|
table: "users",
|
|
891
900
|
expected,
|
|
892
|
-
actual:
|
|
893
|
-
}
|
|
901
|
+
actual: keys
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
break orderPhase;
|
|
905
|
+
}
|
|
906
|
+
const scanned = [];
|
|
907
|
+
for await (const row of driver.scan("users")) scanned.push(row);
|
|
908
|
+
const scannedIds = scanned.map((row) => row.id);
|
|
909
|
+
await driver.close();
|
|
910
|
+
if (!deepEqual(scannedIds, expected)) yield {
|
|
911
|
+
check: "scan-order",
|
|
912
|
+
message: "scan must yield rows in ascending key order",
|
|
913
|
+
context: {
|
|
914
|
+
table: "users",
|
|
915
|
+
expected,
|
|
916
|
+
actual: scannedIds
|
|
894
917
|
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
918
|
+
};
|
|
919
|
+
} catch (error) {
|
|
920
|
+
yield {
|
|
921
|
+
check: "order",
|
|
922
|
+
message: error instanceof Error ? error.message : String(error),
|
|
923
|
+
context: { error }
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
clearPhase: try {
|
|
927
|
+
const driver = factory();
|
|
928
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
929
|
+
await driver.write("users", "u1", {
|
|
930
|
+
id: "u1",
|
|
931
|
+
name: "Ada",
|
|
932
|
+
age: 30
|
|
933
|
+
});
|
|
934
|
+
await driver.write("posts", "p1", {
|
|
935
|
+
slug: "p1",
|
|
936
|
+
title: "Post"
|
|
937
|
+
});
|
|
938
|
+
await driver.clear("users");
|
|
939
|
+
const usersKeys = await driver.keys("users");
|
|
940
|
+
const postsKeys = await driver.keys("posts");
|
|
941
|
+
await driver.close();
|
|
942
|
+
if (usersKeys.length !== 0) {
|
|
943
|
+
yield {
|
|
944
|
+
check: "clear-target",
|
|
945
|
+
message: "clear must empty the targeted table",
|
|
946
|
+
context: {
|
|
915
947
|
table: "users",
|
|
916
948
|
expected: [],
|
|
917
949
|
actual: usersKeys
|
|
918
|
-
});
|
|
919
|
-
if (postsKeys.length !== 1) return findingOf("clear-other", "clear must not affect other tables", {
|
|
920
|
-
table: "posts",
|
|
921
|
-
expected: 1,
|
|
922
|
-
actual: postsKeys.length
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
check: "snapshot",
|
|
928
|
-
run: async () => {
|
|
929
|
-
const driver = factory();
|
|
930
|
-
await driver.open(CONFORMANCE_SCHEMA);
|
|
931
|
-
const original = {
|
|
932
|
-
id: "u1",
|
|
933
|
-
name: "Ada",
|
|
934
|
-
age: 30
|
|
935
|
-
};
|
|
936
|
-
await driver.write("users", "u1", original);
|
|
937
|
-
const rollback = await driver.snapshot();
|
|
938
|
-
await driver.write("users", "u2", {
|
|
939
|
-
id: "u2",
|
|
940
|
-
name: "Grace",
|
|
941
|
-
age: 40
|
|
942
|
-
});
|
|
943
|
-
await driver.delete("users", "u1");
|
|
944
|
-
await rollback();
|
|
945
|
-
const keys = [...await driver.keys("users")];
|
|
946
|
-
if (!deepEqual(keys, ["u1"])) {
|
|
947
|
-
await driver.close();
|
|
948
|
-
return findingOf("snapshot-rollback", "snapshot rollback must restore the pre-snapshot key set", {
|
|
949
|
-
table: "users",
|
|
950
|
-
expected: ["u1"],
|
|
951
|
-
actual: keys
|
|
952
|
-
});
|
|
953
950
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
951
|
+
};
|
|
952
|
+
break clearPhase;
|
|
953
|
+
}
|
|
954
|
+
if (postsKeys.length !== 1) yield {
|
|
955
|
+
check: "clear-other",
|
|
956
|
+
message: "clear must not affect other tables",
|
|
957
|
+
context: {
|
|
958
|
+
table: "posts",
|
|
959
|
+
expected: 1,
|
|
960
|
+
actual: postsKeys.length
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
} catch (error) {
|
|
964
|
+
yield {
|
|
965
|
+
check: "clear",
|
|
966
|
+
message: error instanceof Error ? error.message : String(error),
|
|
967
|
+
context: { error }
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
snapshotPhase: try {
|
|
971
|
+
const driver = factory();
|
|
972
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
973
|
+
const original = {
|
|
974
|
+
id: "u1",
|
|
975
|
+
name: "Ada",
|
|
976
|
+
age: 30
|
|
977
|
+
};
|
|
978
|
+
await driver.write("users", "u1", original);
|
|
979
|
+
const rollback = await driver.snapshot();
|
|
980
|
+
await driver.write("users", "u2", {
|
|
981
|
+
id: "u2",
|
|
982
|
+
name: "Grace",
|
|
983
|
+
age: 40
|
|
984
|
+
});
|
|
985
|
+
await driver.delete("users", "u1");
|
|
986
|
+
await rollback();
|
|
987
|
+
const keys = [...await driver.keys("users")];
|
|
988
|
+
if (!deepEqual(keys, ["u1"])) {
|
|
989
|
+
await driver.close();
|
|
990
|
+
yield {
|
|
991
|
+
check: "snapshot-rollback",
|
|
992
|
+
message: "snapshot rollback must restore the pre-snapshot key set",
|
|
993
|
+
context: {
|
|
957
994
|
table: "users",
|
|
958
|
-
expected:
|
|
959
|
-
actual:
|
|
960
|
-
}
|
|
995
|
+
expected: ["u1"],
|
|
996
|
+
actual: keys
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
break snapshotPhase;
|
|
1000
|
+
}
|
|
1001
|
+
const restored = await driver.read("users", "u1");
|
|
1002
|
+
await driver.close();
|
|
1003
|
+
if (restored === void 0 || !deepEqual(restored, original)) yield {
|
|
1004
|
+
check: "snapshot-rollback-value",
|
|
1005
|
+
message: "snapshot rollback must restore pre-snapshot row values",
|
|
1006
|
+
context: {
|
|
1007
|
+
table: "users",
|
|
1008
|
+
expected: original,
|
|
1009
|
+
actual: restored
|
|
961
1010
|
}
|
|
962
|
-
}
|
|
963
|
-
|
|
1011
|
+
};
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
yield {
|
|
1014
|
+
check: "snapshot",
|
|
1015
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1016
|
+
context: { error }
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const driver = factory();
|
|
1021
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1022
|
+
const original = {
|
|
1023
|
+
id: "u3",
|
|
1024
|
+
name: "Nested",
|
|
1025
|
+
age: 20,
|
|
1026
|
+
meta: { tags: ["a"] }
|
|
1027
|
+
};
|
|
1028
|
+
await driver.write("users", "u3", original);
|
|
1029
|
+
const rollback = await driver.snapshot();
|
|
1030
|
+
const before = await driver.read("users", "u3");
|
|
1031
|
+
if ((0, _orkestrel_contract.isRecord)(before) && (0, _orkestrel_contract.isRecord)(before.meta) && Array.isArray(before.meta.tags)) before.meta.tags.push("mutated-before-restore");
|
|
1032
|
+
await driver.write("users", "u3", {
|
|
1033
|
+
id: "u3",
|
|
1034
|
+
name: "Nested",
|
|
1035
|
+
age: 20,
|
|
1036
|
+
meta: { tags: ["a", "mutated-after-write"] }
|
|
1037
|
+
});
|
|
1038
|
+
await rollback();
|
|
1039
|
+
const restored = await driver.read("users", "u3");
|
|
1040
|
+
await driver.close();
|
|
1041
|
+
if (restored === void 0 || !deepEqual(restored, original)) yield {
|
|
964
1042
|
check: "snapshot-nested",
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
name: "Nested",
|
|
971
|
-
age: 20,
|
|
972
|
-
meta: { tags: ["a"] }
|
|
973
|
-
};
|
|
974
|
-
await driver.write("users", "u3", original);
|
|
975
|
-
const rollback = await driver.snapshot();
|
|
976
|
-
const before = await driver.read("users", "u3");
|
|
977
|
-
if ((0, _orkestrel_contract.isRecord)(before) && (0, _orkestrel_contract.isRecord)(before.meta) && Array.isArray(before.meta.tags)) before.meta.tags.push("mutated-before-restore");
|
|
978
|
-
await driver.write("users", "u3", {
|
|
979
|
-
id: "u3",
|
|
980
|
-
name: "Nested",
|
|
981
|
-
age: 20,
|
|
982
|
-
meta: { tags: ["a", "mutated-after-write"] }
|
|
983
|
-
});
|
|
984
|
-
await rollback();
|
|
985
|
-
const restored = await driver.read("users", "u3");
|
|
986
|
-
await driver.close();
|
|
987
|
-
if (restored === void 0 || !deepEqual(restored, original)) return findingOf("snapshot-nested", "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row", {
|
|
988
|
-
table: "users",
|
|
989
|
-
expected: original,
|
|
990
|
-
actual: restored
|
|
991
|
-
});
|
|
1043
|
+
message: "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row",
|
|
1044
|
+
context: {
|
|
1045
|
+
table: "users",
|
|
1046
|
+
expected: original,
|
|
1047
|
+
actual: restored
|
|
992
1048
|
}
|
|
993
|
-
}
|
|
994
|
-
|
|
1049
|
+
};
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
yield {
|
|
1052
|
+
check: "snapshot-nested",
|
|
1053
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1054
|
+
context: { error }
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const driver = factory();
|
|
1059
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1060
|
+
await driver.write("posts", "hello-world", {
|
|
1061
|
+
slug: "hello-world",
|
|
1062
|
+
title: "Hello"
|
|
1063
|
+
});
|
|
1064
|
+
const post = await driver.read("posts", "hello-world");
|
|
1065
|
+
const key = post === void 0 ? void 0 : extractKey(post, "slug");
|
|
1066
|
+
await driver.close();
|
|
1067
|
+
if (key !== "hello-world") yield {
|
|
995
1068
|
check: "non-id-primary",
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1069
|
+
message: "a non-id primary key column must round-trip through the store",
|
|
1070
|
+
context: {
|
|
1071
|
+
table: "posts",
|
|
1072
|
+
expected: "hello-world",
|
|
1073
|
+
actual: key
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
yield {
|
|
1078
|
+
check: "non-id-primary",
|
|
1079
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1080
|
+
context: { error }
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
const driver = factory();
|
|
1085
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1086
|
+
const nested = {
|
|
1087
|
+
id: "u3",
|
|
1088
|
+
name: "Nested",
|
|
1089
|
+
age: 20,
|
|
1090
|
+
meta: {
|
|
1091
|
+
tags: ["a", "b"],
|
|
1092
|
+
deep: { flag: true }
|
|
1011
1093
|
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1094
|
+
};
|
|
1095
|
+
await driver.write("users", "u3", nested);
|
|
1096
|
+
const readBack = await driver.read("users", "u3");
|
|
1097
|
+
await driver.close();
|
|
1098
|
+
if (readBack === void 0 || !deepEqual(readBack, nested)) yield {
|
|
1014
1099
|
check: "nested-roundtrip",
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1100
|
+
message: "a nested-object row must round-trip structurally",
|
|
1101
|
+
context: {
|
|
1102
|
+
table: "users",
|
|
1103
|
+
expected: nested,
|
|
1104
|
+
actual: readBack
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
yield {
|
|
1109
|
+
check: "nested-roundtrip",
|
|
1110
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1111
|
+
context: { error }
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
migratePhase: try {
|
|
1115
|
+
const driver = factory();
|
|
1116
|
+
if (driver.migrate === void 0) break migratePhase;
|
|
1117
|
+
const deployedUsers = {
|
|
1118
|
+
...CONFORMANCE_USERS_SCHEMA,
|
|
1119
|
+
columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
|
|
1120
|
+
name: "legacy",
|
|
1121
|
+
type: "boolean",
|
|
1122
|
+
nullable: true
|
|
1123
|
+
}]
|
|
1124
|
+
};
|
|
1125
|
+
await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
|
|
1126
|
+
await driver.write("users", "u1", {
|
|
1127
|
+
id: "u1",
|
|
1128
|
+
name: "Ada",
|
|
1129
|
+
age: 30,
|
|
1130
|
+
legacy: true
|
|
1131
|
+
});
|
|
1132
|
+
const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
|
|
1133
|
+
await driver.migrate(removePlan);
|
|
1134
|
+
const migrated = await driver.read("users", "u1");
|
|
1135
|
+
if (migrated === void 0 || "legacy" in migrated) {
|
|
1136
|
+
await driver.close();
|
|
1137
|
+
yield {
|
|
1138
|
+
check: "migrate-column-remove",
|
|
1139
|
+
message: "a column.remove migration must strip the column from stored rows",
|
|
1140
|
+
context: {
|
|
1031
1141
|
table: "users",
|
|
1032
|
-
expected:
|
|
1033
|
-
actual:
|
|
1034
|
-
}
|
|
1142
|
+
expected: void 0,
|
|
1143
|
+
actual: migrated === void 0 ? void 0 : migrated.legacy
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
break migratePhase;
|
|
1147
|
+
}
|
|
1148
|
+
let caught;
|
|
1149
|
+
try {
|
|
1150
|
+
await driver.migrate({
|
|
1151
|
+
from: 0,
|
|
1152
|
+
to: 1,
|
|
1153
|
+
steps: [{
|
|
1154
|
+
operation: "table.remove",
|
|
1155
|
+
table: "ghost"
|
|
1156
|
+
}]
|
|
1157
|
+
});
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
caught = error;
|
|
1160
|
+
}
|
|
1161
|
+
await driver.close();
|
|
1162
|
+
if (!isDatabaseError(caught) || caught.code !== "MIGRATION") yield {
|
|
1163
|
+
check: "migrate-unknown-table",
|
|
1164
|
+
message: "a migration step referencing an unknown table must throw a MIGRATION DatabaseError",
|
|
1165
|
+
context: {
|
|
1166
|
+
table: "ghost",
|
|
1167
|
+
expected: "MIGRATION",
|
|
1168
|
+
actual: isDatabaseError(caught) ? caught.code : caught
|
|
1035
1169
|
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1170
|
+
};
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
yield {
|
|
1038
1173
|
check: "migrate",
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1174
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1175
|
+
context: { error }
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
streamPhase: try {
|
|
1179
|
+
const driver = factory();
|
|
1180
|
+
if (driver.stream === void 0) break streamPhase;
|
|
1181
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1182
|
+
for (const row of [
|
|
1183
|
+
{
|
|
1184
|
+
id: "a",
|
|
1185
|
+
name: "A",
|
|
1186
|
+
age: 10
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
id: "b",
|
|
1190
|
+
name: "B",
|
|
1191
|
+
age: 20
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
id: "c",
|
|
1195
|
+
name: "C",
|
|
1196
|
+
age: 30
|
|
1197
|
+
}
|
|
1198
|
+
]) await driver.write("users", row.id, row);
|
|
1199
|
+
const criteria = { conditions: [{
|
|
1200
|
+
column: "age",
|
|
1201
|
+
operator: "above",
|
|
1202
|
+
values: [10],
|
|
1203
|
+
connector: "and"
|
|
1204
|
+
}] };
|
|
1205
|
+
const matched = [];
|
|
1206
|
+
for await (const row of driver.stream("users", criteria)) matched.push(row);
|
|
1207
|
+
const matchedIds = matched.map((row) => row.id).sort();
|
|
1208
|
+
if (!deepEqual(matchedIds, ["b", "c"])) {
|
|
1209
|
+
await driver.close();
|
|
1210
|
+
yield {
|
|
1211
|
+
check: "stream-match",
|
|
1212
|
+
message: "stream must yield only condition-matching rows",
|
|
1213
|
+
context: {
|
|
1214
|
+
table: "users",
|
|
1215
|
+
expected: ["b", "c"],
|
|
1216
|
+
actual: matchedIds
|
|
1080
1217
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1218
|
+
};
|
|
1219
|
+
break streamPhase;
|
|
1220
|
+
}
|
|
1221
|
+
const paged = [];
|
|
1222
|
+
for await (const row of driver.stream("users", {
|
|
1223
|
+
offset: 1,
|
|
1224
|
+
limit: 1
|
|
1225
|
+
})) paged.push(row);
|
|
1226
|
+
await driver.close();
|
|
1227
|
+
if (paged.length !== 1) yield {
|
|
1228
|
+
check: "stream-page",
|
|
1229
|
+
message: "stream must honor offset and limit",
|
|
1230
|
+
context: {
|
|
1231
|
+
table: "users",
|
|
1232
|
+
expected: 1,
|
|
1233
|
+
actual: paged.length
|
|
1087
1234
|
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1235
|
+
};
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
yield {
|
|
1090
1238
|
check: "stream",
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const matched = [];
|
|
1119
|
-
for await (const row of driver.stream("users", criteria)) matched.push(row);
|
|
1120
|
-
const matchedIds = matched.map((row) => row.id).sort();
|
|
1121
|
-
if (!deepEqual(matchedIds, ["b", "c"])) {
|
|
1122
|
-
await driver.close();
|
|
1123
|
-
return findingOf("stream-match", "stream must yield only condition-matching rows", {
|
|
1124
|
-
table: "users",
|
|
1125
|
-
expected: ["b", "c"],
|
|
1126
|
-
actual: matchedIds
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
const paged = [];
|
|
1130
|
-
for await (const row of driver.stream("users", {
|
|
1131
|
-
offset: 1,
|
|
1132
|
-
limit: 1
|
|
1133
|
-
})) paged.push(row);
|
|
1134
|
-
await driver.close();
|
|
1135
|
-
if (paged.length !== 1) return findingOf("stream-page", "stream must honor offset and limit", {
|
|
1239
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1240
|
+
context: { error }
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
transactionPhase: try {
|
|
1244
|
+
const driver = factory();
|
|
1245
|
+
if (driver.transaction === void 0) break transactionPhase;
|
|
1246
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1247
|
+
await driver.write("users", "u1", {
|
|
1248
|
+
id: "u1",
|
|
1249
|
+
name: "Ada",
|
|
1250
|
+
age: 30
|
|
1251
|
+
});
|
|
1252
|
+
const committing = await driver.transaction();
|
|
1253
|
+
await driver.write("users", "u2", {
|
|
1254
|
+
id: "u2",
|
|
1255
|
+
name: "Grace",
|
|
1256
|
+
age: 40
|
|
1257
|
+
});
|
|
1258
|
+
await committing.commit();
|
|
1259
|
+
const afterCommit = [...await driver.keys("users")].sort();
|
|
1260
|
+
if (!deepEqual(afterCommit, ["u1", "u2"])) {
|
|
1261
|
+
await driver.close();
|
|
1262
|
+
yield {
|
|
1263
|
+
check: "transaction-commit",
|
|
1264
|
+
message: "transaction commit must persist writes made during the scope",
|
|
1265
|
+
context: {
|
|
1136
1266
|
table: "users",
|
|
1137
|
-
expected:
|
|
1138
|
-
actual:
|
|
1139
|
-
}
|
|
1267
|
+
expected: ["u1", "u2"],
|
|
1268
|
+
actual: afterCommit
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
break transactionPhase;
|
|
1272
|
+
}
|
|
1273
|
+
const rollingBack = await driver.transaction();
|
|
1274
|
+
await driver.write("users", "u3", {
|
|
1275
|
+
id: "u3",
|
|
1276
|
+
name: "Marie",
|
|
1277
|
+
age: 50
|
|
1278
|
+
});
|
|
1279
|
+
await rollingBack.rollback();
|
|
1280
|
+
const afterRollback = [...await driver.keys("users")].sort();
|
|
1281
|
+
await driver.close();
|
|
1282
|
+
if (!deepEqual(afterRollback, ["u1", "u2"])) yield {
|
|
1283
|
+
check: "transaction-rollback",
|
|
1284
|
+
message: "transaction rollback must restore pre-transaction state",
|
|
1285
|
+
context: {
|
|
1286
|
+
table: "users",
|
|
1287
|
+
expected: ["u1", "u2"],
|
|
1288
|
+
actual: afterRollback
|
|
1140
1289
|
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1290
|
+
};
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
yield {
|
|
1143
1293
|
check: "transaction",
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if (!deepEqual(afterCommit, ["u1", "u2"])) {
|
|
1162
|
-
await driver.close();
|
|
1163
|
-
return findingOf("transaction-commit", "transaction commit must persist writes made during the scope", {
|
|
1164
|
-
table: "users",
|
|
1165
|
-
expected: ["u1", "u2"],
|
|
1166
|
-
actual: afterCommit
|
|
1167
|
-
});
|
|
1294
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1295
|
+
context: { error }
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
metaPhase: try {
|
|
1299
|
+
const driver = factory();
|
|
1300
|
+
if (driver.meta === void 0 || driver.stamp === void 0) break metaPhase;
|
|
1301
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1302
|
+
const fresh = await driver.meta();
|
|
1303
|
+
if (fresh !== void 0) {
|
|
1304
|
+
await driver.close();
|
|
1305
|
+
yield {
|
|
1306
|
+
check: "meta-fresh",
|
|
1307
|
+
message: "a fresh store must report undefined meta",
|
|
1308
|
+
context: {
|
|
1309
|
+
expected: void 0,
|
|
1310
|
+
actual: fresh
|
|
1168
1311
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
expected: ["u1", "u2"],
|
|
1181
|
-
actual: afterRollback
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
},
|
|
1185
|
-
{
|
|
1312
|
+
};
|
|
1313
|
+
break metaPhase;
|
|
1314
|
+
}
|
|
1315
|
+
const stamped = {
|
|
1316
|
+
version: 1,
|
|
1317
|
+
schema: CONFORMANCE_SCHEMA
|
|
1318
|
+
};
|
|
1319
|
+
await driver.stamp(stamped);
|
|
1320
|
+
const read = await driver.meta();
|
|
1321
|
+
await driver.close();
|
|
1322
|
+
if (read === void 0 || !deepEqual(read, stamped)) yield {
|
|
1186
1323
|
check: "meta-stamp",
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
const fresh = await driver.meta();
|
|
1192
|
-
if (fresh !== void 0) {
|
|
1193
|
-
await driver.close();
|
|
1194
|
-
return findingOf("meta-fresh", "a fresh store must report undefined meta", {
|
|
1195
|
-
expected: void 0,
|
|
1196
|
-
actual: fresh
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
const stamped = {
|
|
1200
|
-
version: 1,
|
|
1201
|
-
schema: CONFORMANCE_SCHEMA
|
|
1202
|
-
};
|
|
1203
|
-
await driver.stamp(stamped);
|
|
1204
|
-
const read = await driver.meta();
|
|
1205
|
-
await driver.close();
|
|
1206
|
-
if (read === void 0 || !deepEqual(read, stamped)) return findingOf("meta-stamp", "meta() must return exactly the last-stamped value", {
|
|
1207
|
-
expected: stamped,
|
|
1208
|
-
actual: read
|
|
1209
|
-
});
|
|
1324
|
+
message: "meta() must return exactly the last-stamped value",
|
|
1325
|
+
context: {
|
|
1326
|
+
expected: stamped,
|
|
1327
|
+
actual: read
|
|
1210
1328
|
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1329
|
+
};
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
yield {
|
|
1332
|
+
check: "meta-stamp",
|
|
1333
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1334
|
+
context: { error }
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
scopedPhase: try {
|
|
1338
|
+
const driver = factory();
|
|
1339
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1340
|
+
await driver.write("users", "u1", {
|
|
1341
|
+
id: "u1",
|
|
1342
|
+
name: "Ada",
|
|
1343
|
+
age: 30
|
|
1344
|
+
});
|
|
1345
|
+
await driver.write("posts", "p1", {
|
|
1346
|
+
slug: "p1",
|
|
1347
|
+
title: "Post"
|
|
1348
|
+
});
|
|
1349
|
+
const rollback = await driver.snapshot(["users"]);
|
|
1350
|
+
await driver.write("users", "u2", {
|
|
1351
|
+
id: "u2",
|
|
1352
|
+
name: "Grace",
|
|
1353
|
+
age: 40
|
|
1354
|
+
});
|
|
1355
|
+
await driver.write("posts", "p2", {
|
|
1356
|
+
slug: "p2",
|
|
1357
|
+
title: "Another post"
|
|
1358
|
+
});
|
|
1359
|
+
await rollback();
|
|
1360
|
+
const usersKeys = [...await driver.keys("users")];
|
|
1361
|
+
if (!deepEqual(usersKeys, ["u1"])) {
|
|
1362
|
+
await driver.close();
|
|
1363
|
+
yield {
|
|
1364
|
+
check: "snapshot-scoped-users",
|
|
1365
|
+
message: "a scoped snapshot must roll back only the named table",
|
|
1366
|
+
context: {
|
|
1367
|
+
table: "users",
|
|
1368
|
+
expected: ["u1"],
|
|
1369
|
+
actual: usersKeys
|
|
1245
1370
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (!deepEqual(postsKeys, ["p1", "p2"])) return findingOf("snapshot-scoped-posts", "a scoped snapshot must leave an unnamed table's mutations intact", {
|
|
1249
|
-
table: "posts",
|
|
1250
|
-
expected: ["p1", "p2"],
|
|
1251
|
-
actual: postsKeys
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1371
|
+
};
|
|
1372
|
+
break scopedPhase;
|
|
1254
1373
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1374
|
+
const postsKeys = [...await driver.keys("posts")].sort();
|
|
1375
|
+
await driver.close();
|
|
1376
|
+
if (!deepEqual(postsKeys, ["p1", "p2"])) yield {
|
|
1377
|
+
check: "snapshot-scoped-posts",
|
|
1378
|
+
message: "a scoped snapshot must leave an unnamed table's mutations intact",
|
|
1379
|
+
context: {
|
|
1380
|
+
table: "posts",
|
|
1381
|
+
expected: ["p1", "p2"],
|
|
1382
|
+
actual: postsKeys
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1259
1385
|
} catch (error) {
|
|
1260
|
-
yield
|
|
1386
|
+
yield {
|
|
1387
|
+
check: "snapshot-scoped",
|
|
1388
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1389
|
+
context: { error }
|
|
1390
|
+
};
|
|
1261
1391
|
}
|
|
1262
1392
|
}
|
|
1263
1393
|
/**
|
|
@@ -1344,10 +1474,12 @@ async function auditDriver(factory) {
|
|
|
1344
1474
|
* ```
|
|
1345
1475
|
*/
|
|
1346
1476
|
function generateUUID(random = Math.random) {
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1477
|
+
const hex = Array.from({ length: 16 }, (_, index) => {
|
|
1478
|
+
const byte = Math.floor(random() * 256) & 255;
|
|
1479
|
+
if (index === 6) return byte & 15 | 64;
|
|
1480
|
+
if (index === 8) return byte & 63 | 128;
|
|
1481
|
+
return byte;
|
|
1482
|
+
}).map((byte) => byte.toString(16).padStart(2, "0"));
|
|
1351
1483
|
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
1352
1484
|
}
|
|
1353
1485
|
//#endregion
|
|
@@ -1384,7 +1516,12 @@ var Cursor = class {
|
|
|
1384
1516
|
if (this.#closed) return;
|
|
1385
1517
|
this.#index += 1;
|
|
1386
1518
|
while (this.#index < this.#keys.length) {
|
|
1387
|
-
const
|
|
1519
|
+
const key = this.#keys[this.#index];
|
|
1520
|
+
if (key === void 0) {
|
|
1521
|
+
this.#index += 1;
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
const row = await this.#table.get(key);
|
|
1388
1525
|
if (row !== void 0) {
|
|
1389
1526
|
this.#value = row;
|
|
1390
1527
|
return;
|
|
@@ -1396,12 +1533,15 @@ var Cursor = class {
|
|
|
1396
1533
|
async update(changes) {
|
|
1397
1534
|
if (this.#closed || this.#value === void 0) return;
|
|
1398
1535
|
const key = this.#keys[this.#index];
|
|
1536
|
+
if (key === void 0) return;
|
|
1399
1537
|
await this.#table.update(key, changes);
|
|
1400
1538
|
this.#value = await this.#table.get(key);
|
|
1401
1539
|
}
|
|
1402
1540
|
async remove() {
|
|
1403
1541
|
if (this.#closed || this.#value === void 0) return;
|
|
1404
|
-
|
|
1542
|
+
const key = this.#keys[this.#index];
|
|
1543
|
+
if (key === void 0) return;
|
|
1544
|
+
await this.#table.remove(key);
|
|
1405
1545
|
this.#value = void 0;
|
|
1406
1546
|
}
|
|
1407
1547
|
close() {
|
|
@@ -1545,8 +1685,8 @@ var Query = class {
|
|
|
1545
1685
|
if (this.#filters.length === 0) return this.#table.records({
|
|
1546
1686
|
conditions: this.#conditions,
|
|
1547
1687
|
order: this.#orders,
|
|
1548
|
-
limit: this.#limit,
|
|
1549
|
-
offset: this.#offset
|
|
1688
|
+
...this.#limit !== void 0 ? { limit: this.#limit } : {},
|
|
1689
|
+
...this.#offset !== void 0 ? { offset: this.#offset } : {}
|
|
1550
1690
|
});
|
|
1551
1691
|
const fetched = await this.#table.records({
|
|
1552
1692
|
conditions: this.#conditions,
|
|
@@ -1579,8 +1719,8 @@ var Query = class {
|
|
|
1579
1719
|
if (this.#filters.length === 0) {
|
|
1580
1720
|
yield* this.#table.scan({
|
|
1581
1721
|
conditions: this.#conditions,
|
|
1582
|
-
limit: this.#limit,
|
|
1583
|
-
offset: this.#offset
|
|
1722
|
+
...this.#limit !== void 0 ? { limit: this.#limit } : {},
|
|
1723
|
+
...this.#offset !== void 0 ? { offset: this.#offset } : {}
|
|
1584
1724
|
}, options);
|
|
1585
1725
|
return;
|
|
1586
1726
|
}
|
|
@@ -1678,8 +1818,8 @@ var Table = class {
|
|
|
1678
1818
|
this.#guard = contract.is;
|
|
1679
1819
|
this.#generate = generate;
|
|
1680
1820
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1681
|
-
on,
|
|
1682
|
-
error
|
|
1821
|
+
...on !== void 0 ? { on } : {},
|
|
1822
|
+
...error !== void 0 ? { error } : {}
|
|
1683
1823
|
});
|
|
1684
1824
|
}
|
|
1685
1825
|
get emitter() {
|
|
@@ -1985,8 +2125,8 @@ var Database = class Database {
|
|
|
1985
2125
|
this.#generate = options.key;
|
|
1986
2126
|
this.#version = options.version;
|
|
1987
2127
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1988
|
-
on: options.on,
|
|
1989
|
-
error: options.error
|
|
2128
|
+
...options.on !== void 0 ? { on: options.on } : {},
|
|
2129
|
+
...options.error !== void 0 ? { error: options.error } : {}
|
|
1990
2130
|
});
|
|
1991
2131
|
}
|
|
1992
2132
|
get emitter() {
|
|
@@ -2000,7 +2140,8 @@ var Database = class Database {
|
|
|
2000
2140
|
}
|
|
2001
2141
|
table(name) {
|
|
2002
2142
|
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2003
|
-
|
|
2143
|
+
const columns = this.#columns(name);
|
|
2144
|
+
return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
|
|
2004
2145
|
}
|
|
2005
2146
|
import(tables, keys) {
|
|
2006
2147
|
return this.#spawn(tables, {
|
|
@@ -2011,7 +2152,7 @@ var Database = class Database {
|
|
|
2011
2152
|
export() {
|
|
2012
2153
|
const result = {};
|
|
2013
2154
|
for (const name of Object.keys(this.#tables)) {
|
|
2014
|
-
const columns = this.#
|
|
2155
|
+
const columns = this.#columns(name);
|
|
2015
2156
|
result[name] = {
|
|
2016
2157
|
key: this.#key(name),
|
|
2017
2158
|
columns,
|
|
@@ -2117,14 +2258,18 @@ var Database = class Database {
|
|
|
2117
2258
|
#key(name) {
|
|
2118
2259
|
return this.#keys[name] ?? "id";
|
|
2119
2260
|
}
|
|
2261
|
+
#columns(name) {
|
|
2262
|
+
const columns = this.#tables[name];
|
|
2263
|
+
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
2264
|
+
return columns;
|
|
2265
|
+
}
|
|
2120
2266
|
#schema() {
|
|
2121
2267
|
return Object.keys(this.#tables).map((name) => {
|
|
2122
|
-
const columns = this.#
|
|
2268
|
+
const columns = this.#columns(name);
|
|
2123
2269
|
return {
|
|
2124
2270
|
name,
|
|
2125
2271
|
primary: this.#key(name),
|
|
2126
|
-
columns: Object.
|
|
2127
|
-
const shape = columns[column];
|
|
2272
|
+
columns: Object.entries(columns).map(([column, shape]) => {
|
|
2128
2273
|
return {
|
|
2129
2274
|
name: column,
|
|
2130
2275
|
type: shapeToColumnType(shape),
|
|
@@ -2380,7 +2525,14 @@ var MemoryDriver = class {
|
|
|
2380
2525
|
const store = this.#require(step.table);
|
|
2381
2526
|
const rows = [...store.entries()];
|
|
2382
2527
|
const migrated = migrateRows(rows.map(([, row]) => row), [step]);
|
|
2383
|
-
|
|
2528
|
+
for (const [index, [key]] of rows.entries()) {
|
|
2529
|
+
const row = migrated[index];
|
|
2530
|
+
if (row === void 0) throw new DatabaseError("MIGRATION", "migrate: transformed row is missing", {
|
|
2531
|
+
table: step.table,
|
|
2532
|
+
index
|
|
2533
|
+
});
|
|
2534
|
+
store.set(key, row);
|
|
2535
|
+
}
|
|
2384
2536
|
break;
|
|
2385
2537
|
}
|
|
2386
2538
|
case "index.add":
|