@nshiab/simple-data-analysis-core 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/esm/class/SimpleTable.d.ts +117 -22
  2. package/esm/class/SimpleTable.d.ts.map +1 -1
  3. package/esm/class/SimpleTable.js +172 -104
  4. package/esm/helpers/findGeoColumn.js +1 -1
  5. package/esm/helpers/getProjection.d.ts.map +1 -1
  6. package/esm/helpers/getProjection.js +3 -0
  7. package/esm/helpers/hasGeometryColumn.d.ts +20 -0
  8. package/esm/helpers/hasGeometryColumn.d.ts.map +1 -0
  9. package/esm/helpers/hasGeometryColumn.js +21 -0
  10. package/esm/helpers/writeGeoData.d.ts +9 -0
  11. package/esm/helpers/writeGeoData.d.ts.map +1 -0
  12. package/esm/helpers/writeGeoData.js +110 -0
  13. package/esm/methods/fill.d.ts.map +1 -1
  14. package/esm/methods/fill.js +16 -16
  15. package/esm/methods/fuzzyClean.d.ts +2 -2
  16. package/esm/methods/fuzzyClean.d.ts.map +1 -1
  17. package/esm/methods/fuzzyClean.js +25 -6
  18. package/esm/methods/fuzzyJoin.d.ts +2 -2
  19. package/esm/methods/fuzzyJoin.d.ts.map +1 -1
  20. package/esm/methods/fuzzyJoin.js +3 -3
  21. package/esm/methods/fuzzyJoinQuery.d.ts +1 -1
  22. package/esm/methods/fuzzyJoinQuery.d.ts.map +1 -1
  23. package/esm/methods/fuzzyJoinQuery.js +13 -3
  24. package/esm/methods/loadDataQuery.d.ts +1 -0
  25. package/esm/methods/loadDataQuery.d.ts.map +1 -1
  26. package/esm/methods/loadDataQuery.js +12 -4
  27. package/esm/methods/padQuery.d.ts +5 -0
  28. package/esm/methods/padQuery.d.ts.map +1 -0
  29. package/esm/methods/padQuery.js +14 -0
  30. package/esm/methods/writeGeoDataQuery.d.ts.map +1 -1
  31. package/esm/methods/writeGeoDataQuery.js +4 -1
  32. package/package.json +1 -1
  33. package/script/class/SimpleTable.d.ts +117 -22
  34. package/script/class/SimpleTable.d.ts.map +1 -1
  35. package/script/class/SimpleTable.js +172 -104
  36. package/script/helpers/findGeoColumn.js +1 -1
  37. package/script/helpers/getProjection.d.ts.map +1 -1
  38. package/script/helpers/getProjection.js +3 -0
  39. package/script/helpers/hasGeometryColumn.d.ts +20 -0
  40. package/script/helpers/hasGeometryColumn.d.ts.map +1 -0
  41. package/script/helpers/hasGeometryColumn.js +24 -0
  42. package/script/helpers/writeGeoData.d.ts +9 -0
  43. package/script/helpers/writeGeoData.d.ts.map +1 -0
  44. package/script/helpers/writeGeoData.js +116 -0
  45. package/script/methods/fill.d.ts.map +1 -1
  46. package/script/methods/fill.js +16 -16
  47. package/script/methods/fuzzyClean.d.ts +2 -2
  48. package/script/methods/fuzzyClean.d.ts.map +1 -1
  49. package/script/methods/fuzzyClean.js +25 -6
  50. package/script/methods/fuzzyJoin.d.ts +2 -2
  51. package/script/methods/fuzzyJoin.d.ts.map +1 -1
  52. package/script/methods/fuzzyJoin.js +3 -3
  53. package/script/methods/fuzzyJoinQuery.d.ts +1 -1
  54. package/script/methods/fuzzyJoinQuery.d.ts.map +1 -1
  55. package/script/methods/fuzzyJoinQuery.js +13 -3
  56. package/script/methods/loadDataQuery.d.ts +1 -0
  57. package/script/methods/loadDataQuery.d.ts.map +1 -1
  58. package/script/methods/loadDataQuery.js +12 -4
  59. package/script/methods/padQuery.d.ts +5 -0
  60. package/script/methods/padQuery.d.ts.map +1 -0
  61. package/script/methods/padQuery.js +17 -0
  62. package/script/methods/writeGeoDataQuery.d.ts.map +1 -1
  63. package/script/methods/writeGeoDataQuery.js +4 -1
@@ -0,0 +1,110 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import createDirectory from "./createDirectory.js";
3
+ import getExtension from "./getExtension.js";
4
+ import findGeoColumn from "./findGeoColumn.js";
5
+ import hasGeometryColumn from "./hasGeometryColumn.js";
6
+ import shouldFlipBeforeExport from "./shouldFlipBeforeExport.js";
7
+ import queryDB from "./queryDB.js";
8
+ import writeGeoDataQuery from "../methods/writeGeoDataQuery.js";
9
+ import mergeOptions from "./mergeOptions.js";
10
+ import rewind from "./rewind.js";
11
+ import stringifyDates from "./stringifyDates.js";
12
+ import stringifyDatesInvert from "./stringifyDatesInvert.js";
13
+ import cleanPath from "./cleanPath.js";
14
+ export default async function writeGeoData(table, file, options = {}) {
15
+ createDirectory(file);
16
+ if (!(await hasGeometryColumn(table))) {
17
+ throw new Error("Table contains no geometry columns. Use writeData() instead.");
18
+ }
19
+ const fileExtension = getExtension(file);
20
+ if (fileExtension === "geojson" || fileExtension === "json") {
21
+ let types;
22
+ if (options.formatDates === true) {
23
+ types = await table.getTypes();
24
+ if (Object.values(types).includes("DATE") ||
25
+ Object.values(types).includes("TIMESTAMP")) {
26
+ await stringifyDates(table, types);
27
+ }
28
+ }
29
+ if (typeof options.compression === "boolean") {
30
+ throw new Error("The compression option is not supported for writing GeoJSON files.");
31
+ }
32
+ const geoColumn = await findGeoColumn(table);
33
+ const flip = shouldFlipBeforeExport(table.projections[geoColumn]);
34
+ if (flip) {
35
+ await table.flipCoordinates(geoColumn);
36
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
37
+ table: table.name,
38
+ method: "writeGeoData()",
39
+ parameters: { file, options },
40
+ }));
41
+ await table.flipCoordinates(geoColumn);
42
+ }
43
+ else {
44
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
45
+ table: table.name,
46
+ method: "writeGeoData()",
47
+ parameters: { file, options },
48
+ }));
49
+ }
50
+ if (options.metadata) {
51
+ const fileData = JSON.parse(readFileSync(file, "utf-8"));
52
+ fileData.metadata = options.metadata;
53
+ writeFileSync(file, JSON.stringify(fileData));
54
+ }
55
+ if (options.rewind) {
56
+ const fileData = JSON.parse(readFileSync(file, "utf-8"));
57
+ const fileRewinded = rewind(fileData);
58
+ writeFileSync(file, JSON.stringify(fileRewinded));
59
+ }
60
+ if (types && (Object.values(types).includes("DATE") ||
61
+ Object.values(types).includes("TIMESTAMP"))) {
62
+ await stringifyDatesInvert(table, types);
63
+ }
64
+ }
65
+ else if (fileExtension === "shp") {
66
+ if (typeof options.precision === "number" ||
67
+ typeof options.compression === "boolean" ||
68
+ typeof options.rewind === "boolean" ||
69
+ options.metadata ||
70
+ options.formatDates === true) {
71
+ throw new Error("The following options are not supported for writing SHAPEFILE files: precision, compression, rewind, metadata, and formatDates.");
72
+ }
73
+ const geoColumn = await findGeoColumn(table);
74
+ const flip = shouldFlipBeforeExport(table.projections[geoColumn]);
75
+ if (flip) {
76
+ await table.flipCoordinates(geoColumn);
77
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
78
+ table: table.name,
79
+ method: "writeGeoData()",
80
+ parameters: { file, options },
81
+ }));
82
+ await table.flipCoordinates(geoColumn);
83
+ }
84
+ else {
85
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
86
+ table: table.name,
87
+ method: "writeGeoData()",
88
+ parameters: { file, options },
89
+ }));
90
+ }
91
+ }
92
+ else if (fileExtension === "geoparquet") {
93
+ if (typeof options.precision === "number") {
94
+ throw new Error("The precision option is not supported for writing PARQUET files. Use the .reducePrecision() method.");
95
+ }
96
+ if (typeof options.rewind === "boolean") {
97
+ throw new Error("The rewind option is not supported for writing PARQUET files.");
98
+ }
99
+ await queryDB(table, `COPY "${table.name}" TO '${cleanPath(file)}' WITH (FORMAT PARQUET${options.compression === true ? ", COMPRESSION 'zstd'" : ""}, KV_METADATA {
100
+ projections: '${JSON.stringify(table.projections)}'
101
+ });`, mergeOptions(table, {
102
+ table: table.name,
103
+ method: "writeGeoData()",
104
+ parameters: { file, options },
105
+ }));
106
+ }
107
+ else {
108
+ throw new Error(`Unknown extension ${fileExtension}`);
109
+ }
110
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"fill.d.ts","sourceRoot":"","sources":["../../src/methods/fill.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAEvD,wBAA8B,IAAI,CAChC,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAC1B,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,iBAiFP"}
1
+ {"version":3,"file":"fill.d.ts","sourceRoot":"","sources":["../../src/methods/fill.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAEvD,wBAA8B,IAAI,CAChC,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAC1B,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,iBAmFP"}
@@ -11,24 +11,19 @@ export default async function fill(simpleTable, columns, options = {}) {
11
11
  }
12
12
  if (options.interpolate || options.interpolateBy) {
13
13
  const cols = stringToArray(columns);
14
- let orderCol;
15
- let excludeList;
16
- if (options.interpolateBy) {
17
- orderCol = options.interpolateBy;
18
- excludeList = cols.join(", ");
19
- }
20
- else {
21
- await simpleTable.addRowNumber(tempRowCol);
22
- orderCol = tempRowCol;
23
- excludeList = [`"${tempRowCol}"`, ...cols].join(", ");
24
- }
14
+ // Always add temp row number to preserve original row order in output
15
+ await simpleTable.addRowNumber(tempRowCol);
16
+ // Use interpolateBy for the window function's ORDER BY (correct interpolation math),
17
+ // but always order final output by tempRowCol to preserve input row order
18
+ const windowOrderCol = options.interpolateBy ?? tempRowCol;
19
+ const excludeList = [`"${tempRowCol}"`, ...cols].join(", ");
25
20
  const overClause = categories.length > 0
26
21
  ? `(PARTITION BY ${categories.map((d) => `"${d}"`).join(", ")})`
27
22
  : `()`;
28
23
  const selectList = cols
29
- .map((col) => `fill(${col} ORDER BY "${orderCol}") OVER ${overClause} as ${col}`)
24
+ .map((col) => `fill(${col} ORDER BY "${windowOrderCol}") OVER ${overClause} as ${col}`)
30
25
  .join(", ");
31
- await queryDB(simpleTable, `CREATE OR REPLACE TABLE "${simpleTable.name}" AS SELECT * EXCLUDE(${excludeList}), ${selectList} FROM "${simpleTable.name}" ORDER BY "${orderCol}";`, mergeOptions(simpleTable, {
26
+ await queryDB(simpleTable, `CREATE OR REPLACE TABLE "${simpleTable.name}" AS SELECT * EXCLUDE(${excludeList}), ${selectList} FROM "${simpleTable.name}" ORDER BY "${tempRowCol}";`, mergeOptions(simpleTable, {
32
27
  table: simpleTable.name,
33
28
  method: "fill()",
34
29
  parameters: { columns, ...options },
@@ -49,9 +44,14 @@ export default async function fill(simpleTable, columns, options = {}) {
49
44
  }));
50
45
  }
51
46
  else {
52
- await queryDB(simpleTable, stringToArray(columns)
53
- .map((col) => `CREATE OR REPLACE TABLE "${simpleTable.name}" AS SELECT * EXCLUDE(${col}), COALESCE(${col}, LAG(${col} IGNORE NULLS) OVER()) as ${col} FROM "${simpleTable.name}";`)
54
- .join("\n"), mergeOptions(simpleTable, {
47
+ // Simple path: add temp row number and order by it to preserve input row order
48
+ await simpleTable.addRowNumber(tempRowCol);
49
+ const cols = stringToArray(columns);
50
+ const excludeList = [`"${tempRowCol}"`, ...cols].join(", ");
51
+ const selectList = cols
52
+ .map((col) => `COALESCE(${col}, LAG(${col} IGNORE NULLS) OVER()) as ${col}`)
53
+ .join(", ");
54
+ await queryDB(simpleTable, `CREATE OR REPLACE TABLE "${simpleTable.name}" AS SELECT * EXCLUDE(${excludeList}), ${selectList} FROM "${simpleTable.name}" ORDER BY "${tempRowCol}";`, mergeOptions(simpleTable, {
55
55
  table: simpleTable.name,
56
56
  method: "fill()",
57
57
  parameters: { columns, ...options },
@@ -1,7 +1,7 @@
1
1
  import type SimpleTable from "../class/SimpleTable.js";
2
- export default function fuzzyClean(table: SimpleTable, column: string, newColumn: string, options?: {
2
+ export default function fuzzyClean(table: SimpleTable, column: string, newColumn: string, threshold: number, options?: {
3
3
  method?: "ratio" | "partial_ratio" | "token_sort_ratio" | "token_set_ratio";
4
- threshold?: number;
5
4
  keep?: "mostCommon" | "longestString" | "shortestString" | "mostCentral" | "maxScore";
5
+ preFilterPrefixLen?: number;
6
6
  }): Promise<void>;
7
7
  //# sourceMappingURL=fuzzyClean.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fuzzyClean.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyClean.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAIvD,wBAA8B,UAAU,CACtC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EACH,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EACD,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,UAAU,CAAC;CACX,GACL,OAAO,CAAC,IAAI,CAAC,CAyNf"}
1
+ {"version":3,"file":"fuzzyClean.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyClean.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAIvD,wBAA8B,UAAU,CACtC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EACH,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,IAAI,CAAC,EACD,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,UAAU,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACxB,GACL,OAAO,CAAC,IAAI,CAAC,CAmPf"}
@@ -1,9 +1,18 @@
1
1
  import mergeOptions from "../helpers/mergeOptions.js";
2
2
  import queryDB from "../helpers/queryDB.js";
3
- export default async function fuzzyClean(table, column, newColumn, options = {}) {
3
+ export default async function fuzzyClean(table, column, newColumn, threshold, options = {}) {
4
4
  const method = options.method ?? "ratio";
5
- const threshold = options.threshold ?? 80;
6
5
  const keep = options.keep ?? "mostCommon";
6
+ let onClause = `rapidfuzz_${method}(a.value, b.value) >= ${threshold}`;
7
+ if (method === "ratio") {
8
+ const maxDiffMultiplier = (200 - 2 * threshold) / (200 - threshold);
9
+ onClause +=
10
+ ` AND ABS(LENGTH(a.value) - LENGTH(b.value)) <= ${maxDiffMultiplier} * GREATEST(LENGTH(a.value), LENGTH(b.value))`;
11
+ }
12
+ if (options.preFilterPrefixLen !== undefined) {
13
+ onClause +=
14
+ ` AND SUBSTR(a.value, 1, ${options.preFilterPrefixLen}) = SUBSTR(b.value, 1, ${options.preFilterPrefixLen})`;
15
+ }
7
16
  // Single round trip: compute fuzzy pairs and embed counts for both sides.
8
17
  // Only values that appear in at least one pair above the threshold can be
9
18
  // normalized — singletons need no processing at all.
@@ -22,16 +31,26 @@ export default async function fuzzyClean(table, column, newColumn, options = {})
22
31
  rapidfuzz_${method}(a.value, b.value) AS score
23
32
  FROM uniques a
24
33
  JOIN uniques b
25
- ON rapidfuzz_${method}(a.value, b.value) >= ${threshold}
34
+ ON ${onClause}
26
35
  AND a.value < b.value`, mergeOptions(table, {
27
36
  table: table.name,
28
37
  method: "fuzzyClean()",
29
- parameters: { column, newColumn, options },
38
+ parameters: { column, newColumn, threshold, options },
30
39
  returnDataFrom: "query",
31
40
  }));
32
41
  const pairs = pairsData ?? [];
33
- if (pairs.length === 0)
34
- return; // Nothing to normalize.
42
+ if (pairs.length === 0) {
43
+ if (newColumn !== column) {
44
+ await queryDB(table, `ALTER TABLE "${table.name}" ADD "${newColumn}" VARCHAR;
45
+ UPDATE "${table.name}"
46
+ SET "${newColumn}" = "${column}";`, mergeOptions(table, {
47
+ table: table.name,
48
+ method: "fuzzyClean()",
49
+ parameters: { column, newColumn, threshold, options },
50
+ }));
51
+ }
52
+ return;
53
+ }
35
54
  // Build count map from the pairs — no separate query needed.
36
55
  const countMap = new Map();
37
56
  for (const { left_value, left_cnt, right_value, right_cnt } of pairs) {
@@ -1,8 +1,8 @@
1
1
  import type SimpleTable from "../class/SimpleTable.js";
2
- export default function fuzzyJoin(leftTable: SimpleTable, rightTable: SimpleTable, leftColumn: string, rightColumn: string, options?: {
2
+ export default function fuzzyJoin(leftTable: SimpleTable, rightTable: SimpleTable, leftColumn: string, rightColumn: string, threshold: number, options?: {
3
3
  method?: "ratio" | "partial_ratio" | "token_sort_ratio" | "token_set_ratio";
4
- threshold?: number;
5
4
  similarityColumn?: string;
6
5
  outputTable?: string | boolean;
6
+ preFilterPrefixLen?: number;
7
7
  }): Promise<SimpleTable>;
8
8
  //# sourceMappingURL=fuzzyJoin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fuzzyJoin.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyJoin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAMvD,wBAA8B,SAAS,CACrC,SAAS,EAAE,WAAW,EACtB,UAAU,EAAE,WAAW,EACvB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IACP,MAAM,CAAC,EACH,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B,wBAuFP"}
1
+ {"version":3,"file":"fuzzyJoin.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyJoin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAMvD,wBAA8B,SAAS,CACrC,SAAS,EAAE,WAAW,EACtB,UAAU,EAAE,WAAW,EACvB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EACH,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACxB,wBAwFP"}
@@ -2,7 +2,7 @@ import getIdenticalColumns from "../helpers/getIdenticalColumns.js";
2
2
  import mergeOptions from "../helpers/mergeOptions.js";
3
3
  import queryDB from "../helpers/queryDB.js";
4
4
  import fuzzyJoinQuery from "./fuzzyJoinQuery.js";
5
- export default async function fuzzyJoin(leftTable, rightTable, leftColumn, rightColumn, options = {}) {
5
+ export default async function fuzzyJoin(leftTable, rightTable, leftColumn, rightColumn, threshold, options = {}) {
6
6
  if (leftColumn === rightColumn) {
7
7
  throw new Error(`The leftColumn and rightColumn have the same name "${leftColumn}". Rename one of them before doing the fuzzy join.`);
8
8
  }
@@ -21,13 +21,12 @@ export default async function fuzzyJoin(leftTable, rightTable, leftColumn, right
21
21
  }
22
22
  }
23
23
  const method = options.method ?? "ratio";
24
- const threshold = options.threshold ?? 80;
25
24
  const similarityColumn = options.similarityColumn;
26
25
  const outputTableName = typeof options.outputTable === "string"
27
26
  ? options.outputTable
28
27
  : leftTable.name;
29
28
  const sql = `INSTALL rapidfuzz FROM community; LOAD rapidfuzz;\n` +
30
- fuzzyJoinQuery(leftTable.name, leftColumn, rightTable.name, rightColumn, method, threshold, outputTableName, similarityColumn);
29
+ fuzzyJoinQuery(leftTable.name, leftColumn, rightTable.name, rightColumn, method, threshold, outputTableName, similarityColumn, options.preFilterPrefixLen);
31
30
  await queryDB(leftTable, sql, mergeOptions(leftTable, {
32
31
  table: outputTableName,
33
32
  method: "fuzzyJoin()",
@@ -35,6 +34,7 @@ export default async function fuzzyJoin(leftTable, rightTable, leftColumn, right
35
34
  leftColumn,
36
35
  rightColumn,
37
36
  rightTable: rightTable.name,
37
+ threshold,
38
38
  options,
39
39
  },
40
40
  }));
@@ -1,2 +1,2 @@
1
- export default function fuzzyJoinQuery(leftTable: string, leftColumn: string, rightTable: string, rightColumn: string, method: "ratio" | "partial_ratio" | "token_sort_ratio" | "token_set_ratio", threshold: number, outputTable: string, similarityColumn: string | undefined): string;
1
+ export default function fuzzyJoinQuery(leftTable: string, leftColumn: string, rightTable: string, rightColumn: string, method: "ratio" | "partial_ratio" | "token_sort_ratio" | "token_set_ratio", threshold: number, outputTable: string, similarityColumn: string | undefined, preFilterPrefixLen?: number): string;
2
2
  //# sourceMappingURL=fuzzyJoinQuery.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fuzzyJoinQuery.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyJoinQuery.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EACF,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,EACrB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GAAG,SAAS,UAsBrC"}
1
+ {"version":3,"file":"fuzzyJoinQuery.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyJoinQuery.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,cAAc,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EACF,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,EACrB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,kBAAkB,CAAC,EAAE,MAAM,UAkC5B"}
@@ -1,11 +1,21 @@
1
- export default function fuzzyJoinQuery(leftTable, leftColumn, rightTable, rightColumn, method, threshold, outputTable, similarityColumn) {
1
+ export default function fuzzyJoinQuery(leftTable, leftColumn, rightTable, rightColumn, method, threshold, outputTable, similarityColumn, preFilterPrefixLen) {
2
2
  const fn = `ROUND(rapidfuzz_${method}("${leftTable}"."${leftColumn}", "${rightTable}"."${rightColumn}"), 2)`;
3
+ let onClause = `${fn} >= ${threshold}`;
4
+ if (method === "ratio") {
5
+ const maxDiffMultiplier = (200 - 2 * threshold) / (200 - threshold);
6
+ onClause +=
7
+ ` AND ABS(LENGTH("${leftTable}"."${leftColumn}") - LENGTH("${rightTable}"."${rightColumn}")) <= ${maxDiffMultiplier} * GREATEST(LENGTH("${leftTable}"."${leftColumn}"), LENGTH("${rightTable}"."${rightColumn}"))`;
8
+ }
9
+ if (preFilterPrefixLen !== undefined) {
10
+ onClause +=
11
+ ` AND SUBSTR("${leftTable}"."${leftColumn}", 1, ${preFilterPrefixLen}) = SUBSTR("${rightTable}"."${rightColumn}", 1, ${preFilterPrefixLen})`;
12
+ }
3
13
  if (similarityColumn) {
4
14
  return `CREATE OR REPLACE TABLE "${outputTable}" AS
5
15
  SELECT * EXCLUDE ("_sda_score"), "_sda_score" AS "${similarityColumn}"
6
16
  FROM (
7
17
  SELECT "${leftTable}".*, "${rightTable}".*, ${fn} AS "_sda_score"
8
- FROM "${leftTable}" LEFT JOIN "${rightTable}" ON ${fn} >= ${threshold}
18
+ FROM "${leftTable}" LEFT JOIN "${rightTable}" ON ${onClause}
9
19
  ) _sda
10
20
  ORDER BY "${leftColumn}", "_sda_score" DESC;\n`;
11
21
  }
@@ -13,7 +23,7 @@ ORDER BY "${leftColumn}", "_sda_score" DESC;\n`;
13
23
  SELECT *
14
24
  FROM (
15
25
  SELECT "${leftTable}".*, "${rightTable}".*
16
- FROM "${leftTable}" LEFT JOIN "${rightTable}" ON ${fn} >= ${threshold}
26
+ FROM "${leftTable}" LEFT JOIN "${rightTable}" ON ${onClause}
17
27
  ) _sda
18
28
  ORDER BY "${leftColumn}", "${rightColumn}";\n`;
19
29
  }
@@ -7,6 +7,7 @@ export default function loadDataQuery(table: string, files: string[], options?:
7
7
  columnTypes?: {
8
8
  [key: string]: string;
9
9
  };
10
+ columns?: string[];
10
11
  header?: boolean;
11
12
  allText?: boolean;
12
13
  delim?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"loadDataQuery.d.ts","sourceRoot":"","sources":["../../src/methods/loadDataQuery.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,OAAO,UAAU,aAAa,CACnC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAExC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,UAAU,CAAC,EAAE,cAAc,GAAG,kBAAkB,GAAG,OAAO,CAAC;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,KAAK,CAAC,EAAE,MAAM,CAAC;CACX,UAwFP"}
1
+ {"version":3,"file":"loadDataQuery.d.ts","sourceRoot":"","sources":["../../src/methods/loadDataQuery.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,OAAO,UAAU,aAAa,CACnC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAExC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,UAAU,CAAC,EAAE,cAAc,GAAG,kBAAkB,GAAG,OAAO,CAAC;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,KAAK,CAAC,EAAE,MAAM,CAAC;CACX,UAmGP"}
@@ -3,6 +3,14 @@ import getExtension from "../helpers/getExtension.js";
3
3
  export default function loadDataQuery(table, files, options = {}) {
4
4
  const fileExtension = getExtension(files[0]);
5
5
  const filesAsString = JSON.stringify(files.map((d) => cleanPath(d)));
6
+ // Column selection: throw for Excel, build SELECT list for others
7
+ const isExcel = options.fileType === "excel" || fileExtension === "xlsx";
8
+ if (options.columns && options.columns.length > 0 && isExcel) {
9
+ throw new Error("The 'columns' option is not supported for Excel files.");
10
+ }
11
+ const selectColumns = options.columns && options.columns.length > 0
12
+ ? options.columns.map((c) => `"${c}"`).join(", ")
13
+ : "*";
6
14
  // General options, except for parquet
7
15
  const autoDetect = typeof options.autoDetect === "boolean"
8
16
  ? `, auto_detect=${String(options.autoDetect).toUpperCase()}`
@@ -44,7 +52,7 @@ export default function loadDataQuery(table, files, options = {}) {
44
52
  const encoding = options.encoding ? `, encoding='${options.encoding}'` : "";
45
53
  const strict = options.strict === false ? `, strict_mode=FALSE` : "";
46
54
  return `CREATE OR REPLACE TABLE "${table}"
47
- AS SELECT * FROM read_csv_auto(${filesAsString}${generalOptions}${header}${allText}${delim}${skip}${compression}${encoding}${strict}${nullPadding}${ignoreErrors})${limit};`;
55
+ AS SELECT ${selectColumns} FROM read_csv_auto(${filesAsString}${generalOptions}${header}${allText}${delim}${skip}${compression}${encoding}${strict}${nullPadding}${ignoreErrors})${limit};`;
48
56
  }
49
57
  else if (options.fileType === "json" || fileExtension === "json") {
50
58
  const jsonFormat = options.jsonFormat
@@ -54,10 +62,10 @@ export default function loadDataQuery(table, files, options = {}) {
54
62
  ? `, records=${String(options.records).toUpperCase()}`
55
63
  : "";
56
64
  return `CREATE OR REPLACE TABLE "${table}"
57
- AS SELECT * FROM read_json_auto(${filesAsString}${generalOptions}${jsonFormat}${records})${limit};`;
65
+ AS SELECT ${selectColumns} FROM read_json_auto(${filesAsString}${generalOptions}${jsonFormat}${records})${limit};`;
58
66
  }
59
67
  else if (options.fileType === "parquet" || fileExtension === "parquet") {
60
- return `CREATE OR REPLACE TABLE "${table}" AS SELECT * FROM read_parquet(${filesAsString}${fileName}${unifyColumns})${limit};`;
68
+ return `CREATE OR REPLACE TABLE "${table}" AS SELECT ${selectColumns} FROM read_parquet(${filesAsString}${fileName}${unifyColumns})${limit};`;
61
69
  }
62
70
  else if (options.fileType === "excel" || fileExtension === "xlsx") {
63
71
  if (files.length > 1) {
@@ -69,7 +77,7 @@ export default function loadDataQuery(table, files, options = {}) {
69
77
  const allText = typeof options.allText === "boolean"
70
78
  ? `, all_varchar=${String(options.allText).toUpperCase()}`
71
79
  : "";
72
- return `CREATE OR REPLACE TABLE "${table}" AS SELECT * FROM read_xlsx('${files[0]}'${options.sheet ? `, sheet='${options.sheet}'` : ""}${header}${allText});`;
80
+ return `CREATE OR REPLACE TABLE "${table}" AS SELECT ${selectColumns} FROM read_xlsx('${files[0]}'${options.sheet ? `, sheet='${options.sheet}'` : ""}${header}${allText});`;
73
81
  }
74
82
  else {
75
83
  throw new Error(`Unknown options.fileType ${options.fileType} or fileExtension ${fileExtension}`);
@@ -0,0 +1,5 @@
1
+ export default function padQuery(table: string, columns: string[], length: number, options: {
2
+ method?: "left" | "right";
3
+ char?: string;
4
+ }): string;
5
+ //# sourceMappingURL=padQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"padQuery.d.ts","sourceRoot":"","sources":["../../src/methods/padQuery.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,UAiBtD"}
@@ -0,0 +1,14 @@
1
+ export default function padQuery(table, columns, length, options) {
2
+ const method = options.method ?? "left";
3
+ const char = options.char ?? "0";
4
+ // Escape single quotes and wrap in single quotes for SQL
5
+ const escapedChar = char.replace(/'/g, "''");
6
+ const paddedChar = `'${escapedChar}'`;
7
+ const func = method === "left" ? "LPAD" : "RPAD";
8
+ let query = "";
9
+ for (const column of columns) {
10
+ query +=
11
+ `\nUPDATE "${table}" SET "${column}" = ${func}("${column}", ${length}, ${paddedChar});`;
12
+ }
13
+ return query;
14
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"writeGeoDataQuery.d.ts","sourceRoot":"","sources":["../../src/methods/writeGeoDataQuery.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACvC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,UAqBrC"}
1
+ {"version":3,"file":"writeGeoDataQuery.d.ts","sourceRoot":"","sources":["../../src/methods/writeGeoDataQuery.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACvC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,UAyBrC"}
@@ -6,10 +6,13 @@ export default function writeGeoDataQuery(table, file, fileExtension, options =
6
6
  layerOptions.push(`COORDINATE_PRECISION=${options.precision}`);
7
7
  }
8
8
  layerOptions.push(`RFC7946=YES`);
9
- return `COPY "${table}" to '${cleanPath(file)}' WITH (FORMAT GDAL, DRIVER 'GeoJSON'${layerOptions.length > 0
9
+ return `INSTALL spatial; LOAD spatial; COPY "${table}" to '${cleanPath(file)}' WITH (FORMAT GDAL, DRIVER 'GeoJSON'${layerOptions.length > 0
10
10
  ? `, LAYER_CREATION_OPTIONS ('WRITE_NAME=NO', ${layerOptions.map((d) => `'${d}'`).join(", ")})`
11
11
  : ""})`;
12
12
  }
13
+ else if (fileExtension === "shp") {
14
+ return `INSTALL spatial; LOAD spatial; COPY "${table}" TO '${cleanPath(file)}' WITH (FORMAT GDAL, DRIVER 'ESRI Shapefile', LAYER_CREATION_OPTIONS 'ENCODING=UTF-8')`;
15
+ }
13
16
  else {
14
17
  throw new Error(`Unknown extension ${fileExtension}`);
15
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nshiab/simple-data-analysis-core",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "The core functionalities of the simple-data-analysis library, with only one dependency: DuckDB.",
5
5
  "repository": {
6
6
  "type": "git",