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