@mason-data/cli 0.7.0 → 0.9.0

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 (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -17,11 +17,15 @@
17
17
  */
18
18
  export const GAPS = {
19
19
  // ---------------------------------------------------------------------------------------------- refused
20
- unverified: { kind: "refused", what: "a function, operator, cast, type or construct the oracle has not held to Postgres", postgres: "answers", mason: "refused, naming each piece", wayOut: "write the statement without it; report it to have it verified", heldBy: "tests/duck-wire.test.ts" },
20
+ "generated-not-built": { kind: "refused", what: "a read of a generated table before its first build ended", postgres: "a materialized view exists from its CREATE", mason: "refused with what the table is doing: being built, waiting for a table to sync, failed or refused", wayOut: "read it once it is built; mason status says how far it is", heldBy: "tests/duck-generated.test.ts" },
21
+ unverified: { kind: "refused", what: "a function, operator, cast, type or construct Mason has not yet verified against Postgres", postgres: "answers", mason: "refused, naming each piece", wayOut: "write the statement without it; report it to have it verified", heldBy: "tests/duck-wire.test.ts" },
21
22
  "explain-option": { kind: "refused", what: "EXPLAIN (FORMAT XML), (FORMAT YAML), (GENERIC_PLAN)", postgres: "the plan in XML or YAML; a plan for unbound parameters", mason: "refused by name: EXPLAIN is DuckDB's plan, in Postgres's text or JSON (docs/DESIGN.md)", wayOut: "FORMAT TEXT or FORMAT JSON; bind the parameters' values and EXPLAIN that" },
22
- "setop-ordinal-star": { kind: "refused", what: "ORDER BY a column number of a UNION / INTERSECT / EXCEPT whose first SELECT lists *", postgres: "sorts by that column", mason: "refused", wayOut: "ORDER BY the column's name" },
23
- "with-ties-distinct": { kind: "refused", what: "FETCH FIRST … WITH TIES over SELECT DISTINCT", postgres: "answers", mason: "refused (DuckDB has no WITH TIES; Mason's rank over DISTINCT is not Postgres's)", wayOut: "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it" },
24
- "collation-unknown": { kind: "refused", what: "COLLATE with a collation other than C, POSIX, default or an ICU locale DuckDB has", postgres: "sorts and compares under it", mason: "refused", wayOut: "COLLATE \"C\" for byte order, or no COLLATE for the database's" },
23
+ "interval-style": { kind: "refused", what: "SET IntervalStyle to postgres_verbose, sql_standard or iso_8601", postgres: "writes every interval in that style", mason: "refused: every interval is written in Postgres's default style (postgres)", wayOut: "SET IntervalStyle = postgres, or to_char(interval, template) for another spelling" },
24
+ "set-config-forms": { kind: "refused", what: "set_config() anywhere but alone in a SELECT of no table with literal arguments: beside other columns or a FROM, in a subquery, with an expression, a parameter or NULL, under EXPLAIN", postgres: "sets the parameter (for the transaction, under is_local) and answers its value", mason: "refused (SELECT set_config('name', 'value', false) alone is the session's SET, and with true its SET LOCAL, answered with its value as SHOW answers it)", wayOut: "SET name = 'value', or SELECT set_config('name', 'value', false) on its own" },
25
+ "star-join-twin": { kind: "refused", what: "a `*` of a named join (`(a JOIN b …) AS j`) with two columns of one name, sorted by an output of the two, or written out over USING, NATURAL or a GROUP BY", postgres: "answers", mason: "refused (no name tells the two apart)", wayOut: "name the join's columns in the SELECT, or leave the join's own name out" },
26
+ "setop-ordinal-star": { kind: "refused", what: "ORDER BY a column number of a UNION / INTERSECT / EXCEPT whose first SELECT lists two columns of that name by a *", postgres: "sorts by that column", mason: "refused", wayOut: "name the columns apart in the first SELECT" },
27
+ "collation-unknown": { kind: "refused", what: "a collation Mason cannot follow: a nondeterministic one anywhere in a statement, and, where text is ordered, an ICU locale with a keyword (`und-u-kn`) or of a language Mason has no collation for", postgres: "sorts, compares and equates under it", mason: "refused (C, POSIX, ucs_basic, the builtin ones, the source's ICU locales and every libc locale, en_US.utf8 included, are followed)", wayOut: "COLLATE \"C\" for byte order, or COLLATE \"und-x-icu\"" },
28
+ "case-mapping-unknown": { kind: "refused", what: "upper, lower or initcap of text under a collation whose case mapping Mason does not follow: a libc locale of Turkish or Azeri (`tr_TR`, `az_AZ`), whose i and I map otherwise, or a builtin locale it does not know", postgres: "maps the letters under that locale", mason: "refused (C, POSIX, ucs_basic, pg_c_utf8, pg_unicode_fast, every ICU locale, Turkish's and Azeri's included, and every other libc locale, en_US.utf8 included, are followed)", wayOut: "COLLATE \"C\" for ASCII letters only, or COLLATE \"und-x-icu\" (\"tr-x-icu\" for Turkish's i)", heldBy: "tests/duck-wire.test.ts" },
25
29
  "like-any-array": { kind: "refused", what: "LIKE / ILIKE ANY (or ALL) over an array that is not written out", postgres: "answers", mason: "refused", wayOut: "write the patterns as ARRAY['…', '…'], or OR the LIKEs" },
26
30
  "similar-to-forms": { kind: "refused", what: "SIMILAR TO with a pattern or ESCAPE that is not a literal, its substring markers, or substring(… SIMILAR … ESCAPE …)", postgres: "answers", mason: "refused", wayOut: "a literal pattern, or ~ with a POSIX regular expression" },
27
31
  "array-remove-type": { kind: "refused", what: "array_remove of an array of a type other than text, integers, floats, booleans, dates or timestamps", postgres: "answers", mason: "refused", wayOut: "array_remove an array of one of those types" },
@@ -33,62 +37,85 @@ export const GAPS = {
33
37
  "json-of-type": { kind: "refused", what: "to_json / array_to_json / json_build_object of a value (or an array of elements) of a type Mason does not write as json", postgres: "answers", mason: "refused", wayOut: "cast the value to text, a number, a boolean, a timestamp or json first" },
34
38
  "extract-julian": { kind: "refused", what: "extract(julian FROM …)", postgres: "answers", mason: "refused", wayOut: "extract(epoch FROM …) / 86400 + 2440587.5" },
35
39
  "with-ordinality": { kind: "refused", what: "a function in FROM … WITH ORDINALITY", postgres: "numbers the rows", mason: "refused", wayOut: "row_number() OVER () instead" },
36
- "array-subquery-element": { kind: "refused", what: "ARRAY(SELECT x …) whose x is a numeric, an array or a row; x = ANY or ALL (ARRAY(SELECT …))", postgres: "a numeric[] computed exactly, a multi-dimensional array (2202E on an empty element, 22004 on a NULL one), a record[]", mason: "refused (DuckDB makes a list of doubles, of lists, of structs; it binds ANY (ARRAY(SELECT …)) as a comparison of its own)", wayOut: "ARRAY(SELECT x::text …), array_agg(x ORDER BY …) in a subquery, or x = ANY (SELECT …) without the ARRAY" },
37
- "multidim-array": { kind: "refused", what: "a multi-dimensional array literal", postgres: "answers", mason: "refused", wayOut: "a one-dimensional array" },
40
+ "array-subquery-element": { kind: "refused", what: "ARRAY(SELECT x …) whose x is a numeric, an array or a row; array_agg(x) of a numeric quotient or average not rounded to a fixed scale (round, trunc or a cast to numeric(p,s)); x = ANY or ALL (ARRAY(SELECT …))", postgres: "a numeric[] computed exactly, a multi-dimensional array (2202E on an empty element, 22004 on a NULL one), a record[]", mason: "refused (DuckDB makes a list of doubles, of lists, of structs; it binds ANY (ARRAY(SELECT …)) as a comparison of its own)", wayOut: "ARRAY(SELECT x::text …) or array_agg(x::text), array_agg(x ORDER BY …) of a column in a subquery, or x = ANY (SELECT …) without the ARRAY" },
41
+ "json-path-type": { kind: "refused", what: "a json read's path cast to a type that changes its text (`varchar(n)[]`, `char(n)[]`, `name[]`)", postgres: "answers", mason: "refused", wayOut: "cast the path to `text[]`" },
42
+ "multidim-array": { kind: "refused", what: "a multi-dimensional array literal, or one bound as a `text[]` parameter", postgres: "answers", mason: "refused", wayOut: "a one-dimensional array" },
43
+ "array-lower-bound": { kind: "refused", what: "a `text[]` parameter whose subscripts do not start at 1 (`[2:3]={a,b}`)", postgres: "answers, its first element at subscript 2", mason: "refused", wayOut: "bind the array without its bounds (`{a,b}`)" },
38
44
  "format-specifiers": { kind: "refused", what: "format() with a specifier other than %s (%I, %L, widths, positions)", postgres: "answers", mason: "refused", wayOut: "concatenate with ||, or %s only" },
39
45
  "to-char-patterns": { kind: "refused", what: "to_char of an interval or a time; its TZ, SP, EEEE, RN, V, B, L patterns; a day or month name padded to 9 characters in a parse", postgres: "answers", mason: "refused", wayOut: "the patterns listed in the hint; extract(… FROM interval) for an interval's parts" },
40
46
  "to-timestamp-format": { kind: "refused", what: "to_timestamp / to_date with a format that is not a literal", postgres: "answers", mason: "refused", wayOut: "write the format as a string literal" },
41
- "libc-uncovered": { kind: "refused", what: "ORDER BY, <, >, min / max of text (or of a jsonb, by its strings) holding a combining mark (e + U+0301), a Thai or Lao vowel written before its consonant, a private-use or unassigned code point, under a libc collation", postgres: "answers, in glibc's order", mason: "refused when a value holding one is ordered (the order is read from the database, and glibc orders these otherwise than Mason can follow)", wayOut: "normalize the text (normalize(x, NFC)), or ORDER BY x COLLATE \"C\" for byte order" },
42
- "libc-symbol-punctuation": { kind: "differs", what: "text ordered under a libc collation where punctuation or a space follows a digit or a symbol and a later letter differs only in case ('1-X' beside '1x')", postgres: "the punctuation outranks the case: 1-X < 1x", mason: "the case outranks the punctuation: 1x < 1-X", wayOut: "ORDER BY lower(x), x when the case must not decide first" },
43
- "percentile-text-libc": { kind: "refused", what: "percentile_disc / mode() WITHIN GROUP (ORDER BY text) under a libc collation", postgres: "answers", mason: "refused", wayOut: "order by a number, or ORDER BY … LIMIT in a subquery" },
47
+ "libc-uncovered": { kind: "refused", what: "ORDER BY, <, >, min / max of text (or of a jsonb, by its strings) holding a character whose order under a libc collation Mason could not read: under a locale other than English's (`en_*`, where it never is, nor is anything refused), a symbol with a first-level weight of its own (`㏩`, `㊿`) or any rare character the read was held to and differed on", postgres: "answers, in the collation's order", mason: "refused when a value holding one is ordered (the order is read from the database, every character held to it, and glibc's every other character is followed); a top N or a min / max orders only the values that may be among its answers (src/duck/libc-bound.ts), so one outside them is not refused", wayOut: "ORDER BY x COLLATE \"C\" for byte order, or normalize(x, NFKC) where it spells the character out", heldBy: "tests/duck-libc-order.test.ts" },
48
+ "libc-locale-unread": { kind: "refused", what: "ORDER BY, <, >, min / max of text (or of a jsonb, by its strings) under a database whose libc collation Mason's read does not follow: a locale that tailors ASCII or Latin (tr_TR, sv_SE, cs_CZ) or another C library's. An `en_*` locale never is; the read says which at the start and mason status repeats it", postgres: "answers, in the collation's order", mason: "refused whenever text is ordered; everything else is answered, equality, GROUP BY and DISTINCT of text included", wayOut: "add COLLATE \"C\" or an ICU collation (COLLATE \"und-x-icu\") to the comparison or the sort", heldBy: "tests/duck-libc-order.test.ts" },
49
+ "fromless-numeric-text-order": { kind: "differs", what: "a SELECT of no table that orders text (`'a' < 'B'`, `… FROM (VALUES …) ORDER BY x`) under a collation other than C and also holds a numeric, a literal such as `1.5` included; or that calls upper, lower or initcap of text and also holds a numeric or a value of a type other than text, a boolean or an integer (a date, a timestamp, a json), or binds a parameter, or a value of the server or the session (`version()`, `current_user`), or what the synced data does not answer as written (`pg_catalog.lower(…)`, `char 'abc'`)", postgres: "the collation's order and case mapping", mason: "the catalog answers it, its numerics exact, its text in byte order and its case mapped by Unicode's rules (`upper('ß')` is `ẞ` where glibc's en_US keeps `ß`; under an ICU collation, an error); a statement of no table of text alone is answered in the collation's order and case mapping", wayOut: "add COLLATE \"C\" to say so, cast the numeric to text or a float, or select FROM a table", heldBy: "tests/integration/duck-oracle.itest.ts" },
50
+ "libc-mark-run-form": { kind: "differs", what: "text under a libc collation holding a combining mark written apart, then a digit or punctuation, then a letter that is a compatibility form at the third level (`ß`, `ð`, `æ`): `'a6ß'` against `'a' || U+0308 || '6ß'`", postgres: "the mark's skipped run moves the third levels one along, and glibc compares a letter's with another's on one scale", mason: "reads each letter's third level as its own (a read of one scale asks the source of every letter), so such two values compare the other way round", wayOut: "ORDER BY normalize(x, NFC), which spells the mark in its letter, or COLLATE \"C\"", heldBy: "tests/integration/duck-oracle.itest.ts" },
51
+ "libc-rare-letter": { kind: "differs", what: "text under an `en_*` collation holding a rare letter or symbol whose weights no spelling of it says, against the same word's near neighbour: `Ꜳ`, `Ꞛ`, `Ꞝ`, `Ꞟ`, an Arabic presentation-form ligature, a Tamil vowel written in two, a CJK compatibility symbol (`㏩`), a circled number (`㊿`, `⑯`)", postgres: "the table's own weight", mason: "the weights the read could confirm: it sorts in its letter's place, and two such neighbours may turn round. mason status says how many pairs of those the read was held to differ", wayOut: "COLLATE \"C\" for byte order, or normalize(x, NFKC) where it spells the letter out", heldBy: "tests/integration/duck-oracle.itest.ts" },
52
+ "hypothetical-set": { kind: "refused", what: "a hypothetical-set aggregate: rank, dense_rank, percent_rank or cume_dist(…) WITHIN GROUP (ORDER BY …)", postgres: "the rank the value would have among the rows", mason: "refused", wayOut: "count the rows below the value in a subquery" },
44
53
  "infinite-extract": { kind: "refused", what: "extract(year, decade, century, millennium, isoyear or epoch FROM) an infinite date or timestamp", postgres: "Infinity (a numeric)", mason: "refused when a row holds one (DuckDB's DECIMAL has no Infinity)", wayOut: "filter the infinite values out (isfinite(x)), or date_part(…), which is a double" },
54
+ "time-timetz-coerced": { kind: "refused", what: "a time Postgres casts to a timetz where Mason cannot write the cast: an arm of a UNION, INTERSECT or EXCEPT written as a * or as a set operation of its own with an ORDER BY or a LIMIT; a time BETWEEN a time bound and a timetz one; a CASE of a time WHEN a time and WHEN a timetz", postgres: "casts the time to a timetz at the session zone's offset on the transaction's date", mason: "refused (a time beside a timetz in a comparison, IN, BETWEEN, CASE, COALESCE, GREATEST, LEAST, NULLIF, an ARRAY, a VALUES list, a function's argument and a set operation's named columns is answered)", wayOut: "cast the time: tm::timetz" },
45
55
  "time-date-trunc": { kind: "refused", what: "date_trunc of a time (Postgres truncates it as an interval)", postgres: "an interval", mason: "refused", wayOut: "date_trunc of a timestamp, or extract the parts" },
46
56
  "quote-ident": { kind: "refused", what: "quote_ident()", postgres: "quotes an identifier when it must be", mason: "refused (it needs Postgres's keyword list)", wayOut: "quote it in the statement, or format('%s', …)" },
47
57
  "div-numeric": { kind: "refused", what: "div() of a value that is not an integer", postgres: "the integer quotient of two numerics", mason: "refused", wayOut: "trunc(a / b)" },
48
58
  "jsonb-set": { kind: "refused", what: "jsonb_set, jsonb_insert, the #- operator, jsonb - of a text[] or an index", postgres: "answers", mason: "refused", wayOut: "build the value with jsonb_build_object" },
49
59
  "jsonb-compare": { kind: "refused", what: "a jsonb compared by GREATEST / LEAST, WITHIN GROUP (ORDER BY …), IN or = ANY of a subquery or of an array not written out, BETWEEN SYMMETRIC, JOIN … USING or NATURAL JOIN, UNION / INTERSECT / EXCEPT (but UNION ALL), SELECT DISTINCT *, GROUPING SETS / ROLLUP / CUBE, or an aggregate's DISTINCT but count's; ORDER BY the number of a jsonb column a * lists; a row holding a jsonb compared, grouped or sorted; a jsonb[] sorted, grouped, compared, contained (@>, <@, &&) or searched (array_position, array_remove)", postgres: "answers, by jsonb's own order and equality", mason: "refused (ORDER BY, GROUP BY, DISTINCT [ON], =, <, >, IN (…), = ANY (ARRAY[…]), BETWEEN, IS DISTINCT FROM, NULLIF, CASE x WHEN, PARTITION BY and count(DISTINCT …) of a jsonb are answered in its order)", wayOut: "compare the jsonb in one of the answered forms, a member (j ->> 'k'), or j::text for the canonical text" },
50
60
  "text-array-order": { kind: "refused", what: "ORDER BY, GROUP BY, DISTINCT, min, max or <, > over a text[] under the database's collation", postgres: "compares the elements under the collation", mason: "refused", wayOut: "sort by an element (a[1]), or by array_to_string(a, ',')" },
61
+ "latest-order": { kind: "refused", what: "latest(x) with no ORDER BY of its own (or only constant keys), or latest as a window function (OVER)", postgres: "answers the value of whichever row it happens to read last", mason: "refused: the row latest() answers is the one its ORDER BY sorts last", wayOut: "latest(x ORDER BY k, id) in a GROUP BY, its keys ending in a unique column" },
62
+ "latest-quotient": { kind: "refused", what: "latest() of a numeric quotient or average (`latest(a / b ORDER BY …)`)", postgres: "answers the exact value", mason: "refused (the engine holds a quotient as a double)", wayOut: "latest(a ORDER BY …) / latest(b ORDER BY …)" },
51
63
  "sum-real": { kind: "refused", what: "sum() of a real (float4), as an aggregate or a window", postgres: "accumulates in single precision", mason: "refused (DuckDB accumulates in double precision)", wayOut: "sum(x::double precision)" },
52
64
  "interval-aggregates": { kind: "refused", what: "sum() or avg() of an interval", postgres: "adds months, days and time apart", mason: "refused", wayOut: "sum(extract(epoch FROM x)) for the seconds" },
53
65
  "bc-timestamptz-zone": { kind: "refused", what: "a timestamptz before year 1 written in a zone other than UTC", postgres: "the zone's local mean time", mason: "refused when a row holds one", wayOut: "SET TimeZone = 'UTC', or filter those rows out", heldBy: "tests/duck-wire.test.ts" },
54
66
  "width-bucket-forms": { kind: "refused", what: "width_bucket with its bounds reversed, or of an array of thresholds", postgres: "answers", mason: "refused", wayOut: "low bound first" },
55
67
  "inet-order": { kind: "refused", what: "ORDER BY, <, >, min or max over an inet", postgres: "orders addresses by network, then host", mason: "refused (equality, IS NULL and the value itself are answered)", wayOut: "sort or compare another column" },
56
- "citext-forms": { kind: "refused", what: "GROUP BY, DISTINCT, min, max, a set operation, or = ANY (an array) over a citext", postgres: "groups and sorts without regard to case, answering one spelling", mason: "refused (=, <>, <, >, IN (…), LIKE and ORDER BY are answered, case-insensitively)", wayOut: "GROUP BY lower(x), or x::text where case matters" },
68
+ "held-type-compare": { kind: "refused", what: "what needs the type's own value of a column Mason holds as its text (money, bit, macaddr, cidr, a geometric type, a range, a tsvector, xml, a composite, an extension's type): ORDER BY, <, >, min, max, sum; a function, an operator, a cast to another type, a subscript or a field; GROUP BY or DISTINCT of a numrange, a tsquery or a tstzrange; = of a money", postgres: "orders and computes by the type's own rules", mason: "refused (the value, x::text, IS NULL, count, and = and GROUP BY where equal values have one text are answered: docs/SQL-SUPPORT.md)", wayOut: "x::text, which sorts, groups and compares as text; or another column" },
69
+ "binary-held-type": { kind: "refused", what: "a column of a type Mason holds as its text, in binary format, but a point or a box", postgres: "the type's binary form", mason: "refused", wayOut: "ask for the column in text format", heldBy: "tests/duck-mapped-wire.test.ts" },
70
+ "citext-forms": { kind: "refused", what: "GROUP BY, DISTINCT, min, max, a set operation, = ANY (an array), ALL of a subquery (`ci < ALL (SELECT …)`), a row ordered by <, <=, > or >= (`(ci, id) < ('ab', 3)`), or a row compared by <, <=, >, >= or <> with ANY of a subquery, over a citext", postgres: "groups and sorts without regard to case, answering one spelling", mason: "refused (=, <>, <, >, IN (…), LIKE and ORDER BY are answered, case-insensitively)", wayOut: "GROUP BY lower(x), or x::text where case matters" },
57
71
  "bytea-forms": { kind: "refused", what: "encode / decode to a format other than hex, base64 or escape, or one not written as a literal; convert_to / convert_from of an encoding other than UTF8", postgres: "answers", mason: "refused", wayOut: "encode(x, 'hex'), 'base64' or 'escape'; the UTF8 encoding" },
58
- "row-value": { kind: "refused", what: "a row as a value outside the SELECT list, a cast to text and the json functions (compared, tested with IS NULL, grouped, sorted); a row of a row; a row of a table Mason copied in part", postgres: "compares, tests and groups a row by its columns", mason: "refused", wayOut: "compare, test or sort its columns" },
72
+ "row-value": { kind: "refused", what: "a row as a value outside the SELECT list, a cast to text and the json functions (compared, tested with IS NULL, grouped, sorted, a row output sorted by its number or name included); a row of a row; a row of a table Mason synced in part; a row holding a numeric quotient or average, anywhere", postgres: "compares, tests and groups a row by its columns", mason: "refused", wayOut: "compare, test or sort its columns" },
59
73
  "zone-abbreviation": { kind: "refused", what: "a time zone written as an abbreviation or a POSIX zone (`CST`, `IST`, `UTC+3`, `+05`) in AT TIME ZONE or date_trunc", postgres: "a fixed offset (`CST` is -06 all year; `UTC+3` is three hours WEST)", mason: "refused, written or held by a row (DuckDB reads several as another zone: `CST` as Chicago's, `IST` as India's, `UTC+3` as east)", wayOut: "an IANA zone name (America/Chicago, Asia/Kolkata), in any case; UT, Z and UTC are UTC", heldBy: "tests/duck-wire.test.ts" },
60
74
  "volatile-window": { kind: "refused", what: "a window ordered or partitioned by random() or another volatile function (a random row per group)", postgres: "a new value per row", mason: "refused (DuckDB refuses a volatile expression there, and computes a deterministic one once for every row)", wayOut: "SELECT …, row_number() OVER (ORDER BY r) FROM (SELECT *, random() AS r FROM t) AS s" },
61
75
  "numeric-special": { kind: "refused", what: "arithmetic on a numeric NaN or Infinity read from a text", postgres: "NaN and ±Infinity by its own rules (NaN + 1 is NaN)", mason: "refused when a row holds one", wayOut: "filter the special values out first (x <> 'NaN')" },
62
76
  "numeric-exact-sum": { kind: "refused", what: "sum() or avg() of a numeric Postgres's own function computes here — a text read as an unconstrained numeric (Metabase's JSON number, `(j ->> 'k')::numeric`), ln, exp, sqrt, power — holding a value of more than 20 integer digits or 36 decimals besides trailing zeros, or summing past 20 integer digits", postgres: "adds every digit", mason: "refused when a row or the sum does (each value is summed exactly, in two DECIMAL(38,18) parts)", wayOut: "cast to numeric(p, s), or round(x, 18) first" },
77
+ "numeric-quotient-equality": { kind: "refused", what: "a numeric quotient or average (a value Mason computes exactly from its operands) in GROUPING SETS, ROLLUP or CUBE, in an aggregate's DISTINCT other than count's, or as a column of UNION, INTERSECT or EXCEPT without ALL (or INTERSECT ALL, EXCEPT ALL); one a subquery or WITH query computed, compared outside it where that subquery is also read by a * listing two columns of one name, a NATURAL JOIN or as a whole row; one a subquery or WITH query computed, joined by JOIN … USING or NATURAL JOIN on it, unless rounded to a fixed scale (round, trunc or a cast to numeric(p,s))", postgres: "compares the exact values", mason: "refused: the engine holds the value as a double, which equates values Postgres tells apart past 17 digits (DISTINCT, GROUP BY, PARTITION BY, count(DISTINCT), = and <> compare the exact value, its text's too, in the query that computes it and outside it)", wayOut: "SELECT DISTINCT over a UNION ALL; EXISTS / NOT EXISTS for INTERSECT / EXCEPT; a GROUP BY per grouping set, joined by UNION ALL; name the subquery's columns in place of its *, join it with ON (… JOIN t ON t.q = s.q in place of USING (q)), read its columns, not its whole row (each refusal's HINT names the one that answers it)" },
78
+ "numeric-double-integer": { kind: "refused", what: "a cast to an integer (::int, ::bigint, CAST(… AS integer)) of a numeric the engine computes as a double that Mason cannot compute exactly: stddev or variance of a numeric without a precision or of more than 18 digits", postgres: "rounds the exact value half away from zero (2.5 is 3)", mason: "refused when a row is read (the engine's own type decides): the double would round half to even (2.5 is 2); a quotient, an avg, round, NULLIF, percentile_disc and mode() of one and the other forms Mason computes exactly, and what the engine holds as a decimal, are cast as Postgres casts them", wayOut: "cast to float8 first, (stddev(x)::float8)::int, which rounds the double half to even as Postgres rounds a float8" },
79
+ "numeric-ordered-set": { kind: "refused", what: "percentile_disc or mode() WITHIN GROUP with an ARRAY of fractions, ordered by a numeric Mason computes exactly that the engine holds as a double (percentile_disc(ARRAY[0.25, 0.75]) WITHIN GROUP (ORDER BY a / b))", postgres: "an array of the picked rows' exact values", mason: "refused by name before the statement runs: the array would hold the engine's doubles (one fraction at a time is answered exactly)", wayOut: "ask for each fraction alone: percentile_disc(0.25) WITHIN GROUP (ORDER BY a / b), percentile_disc(0.75) WITHIN GROUP (ORDER BY a / b)" },
63
80
  "numeric-math": { kind: "refused", what: "ln, log, exp, sqrt or power of a numeric expression whose scale its types do not decide (a division, an unconstrained numeric's arithmetic)", postgres: "digits at a scale from the argument's own", mason: "refused (a column, an integer, a literal and a numeric(p, s) are answered by Postgres's own function)", wayOut: "cast the argument to numeric(p, s)" },
64
81
  "view-forms": { kind: "refused", what: "a view's whole row as a value (`SELECT v FROM v`), a WITH query named as a view the statement reads, a column qualified by its view's schema beside another item of the view's name, views read through more than 16 others", postgres: "the view's row type; the WITH query or the view as each is in scope", mason: "refused (a view is answered by its definition in its place, as Postgres's rewriter answers it; its columns, `v.*` and `public.v.col` are answered)", wayOut: "name the view's columns; give the view or the WITH query another name" },
65
82
  "enum-forms": { kind: "refused", what: "an array of an enum read from a text (`t::mood[]`)", postgres: "reads each element as a label", mason: "refused (a column, a label, an array literal and a text read as one label are answered)", wayOut: "write the array as a literal, or ARRAY[…] of its labels" },
66
83
  "infinite-interval": { kind: "refused", what: "an infinite interval read from a text (Postgres 17+)", postgres: "infinity, -infinity", mason: "refused when a row holds one (DuckDB's interval has no infinity)", wayOut: "filter it out first, or read it as text" },
67
84
  "relative-time-literal": { kind: "refused", what: "a relative time combined with anything else ('today 10:00', 'now'::date + … written as one literal), or read from a row as a date or a time", postgres: "the transaction's start (a row's value: when the row is read)", mason: "refused (a bare 'now', 'today', 'tomorrow', 'yesterday' literal is answered: the transaction's start at the Parse)", wayOut: "current_date + time '10:00', now(), current_date + 1" },
68
85
  // ---------------------------------------------------------------------------------------------- differs
69
- "limit-errors": { kind: "differs", what: "a value that raises (`'x'::int`, a bad JSON number) in a row a LIMIT leaves out: in the select list, where no index gives the ORDER BY; or in the argument of a LEFT JOIN of a list's rows ON TRUE (unnest(ARRAY[…, 'x'::int]), jsonb_each of a non-object), under a LIMIT with no ORDER BY", postgres: "the select list's raises: its plan sorts every row, its select list computed for each (through an index it stops at the limit and answers); the lateral's answers: it stops at the limit", mason: "the select list's answers: a sorted, limited select list is computed for the rows the limit keeps (src/duck/late.ts), as Postgres's index scan does; the lateral's raises: DuckDB computes its argument for a whole vector of rows (2,048) before the limit stops it", wayOut: "none needed for the select list's answer; filter the bad rows out to have neither raise" },
70
- "limit-lateral-filter": { kind: "differs", what: "a LEFT JOIN of a json document's rows ON TRUE (json_each, jsonb_array_elements, …) under a LIMIT, or a row count an Execute sets on the wire, and no ORDER BY, with a filter above the join that DuckDB does not push down to the base: a WHERE on the lateral's own column (d.key <> 'n'), one holding a bound parameter ($1) in a SELECT around the join, or one in a SELECT that reads the join through a CTE", postgres: "the first rows of the base, in its order, each row's members in theirs", mason: "DuckDB plans the lateral under such a filter as a delimiter join and hands the rows back in an order of its own, so the LIMIT keeps other rows (a filter on the base in the join's own SELECT, literal or parameter, or a literal one around it through a derived table, keeps Postgres's)", wayOut: "ORDER BY the base's key before the LIMIT, or filter the base in the SELECT that holds the join", heldBy: "tests/duck-wire.test.ts" },
86
+ "limit-errors": { kind: "differs", what: "a value that raises (`'x'::int`, a bad JSON number) in a row a LIMIT leaves out: in the select list or a plain derived read's list (a view, a WITH query, a tool's wrapper), where no index gives the ORDER BY, a subquery of the select list among them (one Postgres finds cheap, it computes below its sort), and a SELECT DISTINCT ON's list, where the row raising is in a group but not its first; or in the argument of a LEFT JOIN of a list's rows ON TRUE (unnest(ARRAY[…, 'x'::int]), jsonb_each of a non-object), under a LIMIT with no ORDER BY", postgres: "the select list's raises: its plan sorts every row, its select list computed for each (through an index it stops at the limit and answers); the lateral's answers: it stops at the limit", mason: "the select list's answers: a sorted, limited select list, a derived list a join reads by row number, a subquery in the list and a DISTINCT ON's list are computed for the rows the limit keeps (src/duck/late.ts), as Postgres's index scan does; the lateral's raises: DuckDB computes its argument for a whole vector of rows (2,048) before the limit stops it", wayOut: "none needed for the select list's answer; filter the bad rows out to have neither raise" },
87
+ "limit-lateral-filter": { kind: "differs", what: "a LEFT JOIN of a json document's rows ON TRUE (json_each, jsonb_array_elements, …) under a LIMIT, or a row count an Execute sets on the wire, and no ORDER BY, with a filter above the join that is not pushed down to the base: a WHERE on the lateral's own column (d.key <> 'n'), one holding a bound parameter ($1) in a SELECT around the join, or one in a SELECT that reads the join through a CTE", postgres: "the first rows of the base, in its order, each row's members in theirs", mason: "the engine plans the lateral under such a filter as a delimiter join and hands the rows back in an order of its own, so the LIMIT keeps other rows (a filter on the base in the join's own SELECT, literal or parameter, or a literal one around it through a derived table, keeps Postgres's)", wayOut: "ORDER BY the base's key before the LIMIT, or filter the base in the SELECT that holds the join", heldBy: "tests/duck-wire.test.ts" },
71
88
  "limit-lateral-argument": { kind: "differs", what: "a LEFT JOIN of a list's rows ON TRUE (unnest, json_each, jsonb_array_elements, …) under a LIMIT, or a row count an Execute sets on the wire, and no ORDER BY, whose argument holds a subquery (unnest(ARRAY(SELECT …))), or reads two or more columns of the row and no array (CASE WHEN t.kind = 'doc' THEN t.body END) in a SELECT whose list is a bare * or inside a join with a name of its own ((a LEFT JOIN … ON TRUE) AS j)", postgres: "the first rows of the base, in its order, each row's members in theirs", mason: "DuckDB plans such a lateral as a delimiter join and hands the rows back in an order of its own, so the LIMIT keeps other rows (elsewhere an argument of two columns is computed in a step of its own before the lateral, which a * would read as a column; a subquery is a delimiter join in every form)", wayOut: "name the columns instead of *, or ORDER BY the base's key before the LIMIT" },
72
89
  "window-partition-order": { kind: "differs", what: "a window function that reads its frame in order (row_number, lag, lead, first_value, last_value, nth_value, ntile) over a window with PARTITION BY and no ORDER BY", postgres: "a partition's rows in the order its sort hands them back: in practice the order they came in", mason: "a partition's rows in the order DuckDB's partitioning hands them back, another one", wayOut: "ORDER BY in the window (OVER (PARTITION BY a ORDER BY b))" },
73
- "mixed-scale-numeric": { kind: "differs", what: "a numeric column whose values have several scales: an integer UNION ALL a numeric, lead(x, 1, 0) beside numeric(2,1) values, an unconstrained column holding 1 and 1.50", postgres: "each value its own scale: 1, 1.5", mason: "one scale per column: 1.0, 1.5", wayOut: "cast to numeric(p, s), or round(x, s)" },
74
- "unicode-16-case": { kind: "differs", what: "upper / lower of letters added in Unicode 16", postgres: "maps them (its own Unicode 16 tables)", mason: "leaves them as they are (DuckDB's ICU predates Unicode 16)", wayOut: "none needed for letters before Unicode 16" },
90
+ "mixed-scale-numeric": { kind: "differs", what: "a numeric column whose values have several scales: an integer beside a numeric in a UNION, INTERSECT or EXCEPT without ALL, the rows of a VALUES list outside a set operation (VALUES (1), (2.5)), an unconstrained column holding 1 and 1.50 (a UNION ALL's arms and lead or lag's default each keep their own)", postgres: "each value its own scale: 1, 1.5", mason: "one scale per column: 1.0, 1.5", wayOut: "cast to numeric(p, s), or round(x, s)" },
91
+ "unicode-16-case": { kind: "differs", what: "upper / lower of letters added in Unicode 16 under a libc collation (en_US.utf8); initcap beside a letter added in Unicode 17", postgres: "maps the Unicode 16 letters (its own tables, glibc's and ICU's are Unicode 16), and reads a Unicode 17 letter as none, so the letter after it starts a word", mason: "leaves the Unicode 16 letters as they are (DuckDB's ICU predates Unicode 16), and reads a Unicode 17 letter as a letter where Mason's runtime knows Unicode 17", wayOut: "none needed for letters before Unicode 16" },
75
92
  "float-last-digit": { kind: "differs", what: "stddev / variance of a float, and ln, log, exp, power of a double: the last digit may differ", postgres: "its algorithm and its machine's libm", mason: "DuckDB's and its machine's: alike on some pairs of machines, apart in the 16th–17th digit on others", wayOut: "round(x::numeric, 12), or compare within a tolerance (abs(a - b) <= 1e-12 * abs(b))" },
76
93
  "negative-zero": { kind: "differs", what: "-0 beside 0 in greatest / least / percentile_disc", postgres: "keeps the one it met first", mason: "may answer the other", wayOut: "x + 0 makes -0 into 0" },
77
94
  "float-overflow": { kind: "differs", what: "exp / power of a double past its range, or so small it underflows", postgres: "an error (22003)", mason: "Infinity, or 0", wayOut: "bound the argument" },
78
95
  "min-max-scale-tie": { kind: "differs", what: "min / max of a numeric expression (a quotient) where equal values have different scales (0 / 7 and 0 / 1000000)", postgres: "the last of them it reads, so the scan order decides the trailing zeros", mason: "the one whose operands are least (min) or greatest (max)", wayOut: "round(min(a / b), n)" },
79
96
  "numeric-stddev": { kind: "differs", what: "stddev / variance of DISTINCT integers or numerics, or of a numeric declared without a precision or with more than 18 digits: the last digits", postgres: "exact numeric arithmetic, to its own scale", mason: "a double's 15 digits", wayOut: "round(stddev(x), 10)" },
80
97
  "jsonb-equal-text": { kind: "differs", what: "GROUP BY or DISTINCT of jsonb values that are equal but written apart ('1.0' and '1.00', '{\"a\": 1}' and '{\"a\": 1.0}')", postgres: "one group, written as the value its plan reads first", mason: "one group, written as the value DuckDB reads first: either may be any of them", wayOut: "GROUP BY j::text where the text must decide", heldBy: "tests/duck-jsonb-order.test.ts" },
81
- "char-n-minmax": { kind: "differs", what: "greatest / least of a char(n) beside a text", postgres: "compares without the padding, answers the first of equal values", mason: "compares the padded text", wayOut: "cast the char(n) to text first" },
82
- "nested-exact-numeric": { kind: "differs", what: "an expression over a numeric a subquery computed exactly (a division, an avg, a round), outside that subquery: SELECT x * 2 FROM (SELECT a / 3 AS x …)", postgres: "exact digits at every step", mason: "the subquery's value as DuckDB computed it (a double's 15 digits) where the outer query computes with it; passed through as it is (SELECT *, s.x), it is exact", wayOut: "compute it in the outermost query, or round(…, n) inside" },
98
+ "numeric-quotient-compare": { kind: "differs", what: "a numeric quotient or average compared by = ANY, IS DISTINCT FROM, NULLIF, CASE x WHEN, <, >, <= or >=, beside a value equal to it in its first 17 significant digits", postgres: "compares the exact values", mason: "compares the doubles, which are equal (=, <>, IN a list, DISTINCT, GROUP BY, PARTITION BY, count(DISTINCT), ORDER BY, a window's ORDER BY, FETCH … WITH TIES and min / max compare the exact values, and so does a NULLIF whose value is printed, cast or aggregated)", wayOut: "x IN (v, w) or x = v OR x = w in place of = ANY; (x = v) IS NOT TRUE in place of IS DISTINCT FROM; CASE WHEN x = v in place of CASE x WHEN (none for NULLIF or <)" },
99
+ "nested-exact-numeric": { kind: "differs", what: "a numeric quotient or average a subquery computed (SELECT a / 3 AS x …), outside it: aggregated by an aggregate but count, sum, avg, min and max (SELECT stddev(x) FROM (…) s); read as the subquery's whole row; and a UNION ALL column of a quotient's text beside an arm of other text", postgres: "exact digits at every step", mason: "the subquery's value as the engine computed it (a double's 15 digits); read as it is, through arithmetic, a cast, sum, avg, min / max, a GROUP BY, an ORDER BY, a join, an alias list, a DISTINCT, a * beside other columns or a UNION ALL arm, it is exact", wayOut: "compute it in the outermost query, name the subquery's columns, or round(…, n) inside" },
100
+ "domain-column": { kind: "differs", what: "a column of a domain: pg_typeof(x) and information_schema.columns.domain_name", postgres: "the domain's name", mason: "the column is read and declared as its base type, which is what the wire says and what information_schema's data_type says on Postgres too; a cast to the domain's name (`x::my_domain`) is 42704", wayOut: "none needed: a value of the domain is a value of its base type; cast to the base type" },
101
+ "show-all": { kind: "differs", what: "SHOW ALL", postgres: "every server setting with the source database's values", mason: "every setting the listener's catalog Postgres has, with this session's values laid over (search_path, TimeZone, ssl, …): Mason's settings, never your source database's", wayOut: "ask the source database for its settings (SHOW ALL or SHOW <name> there): Mason answers its own" },
83
102
  // ---------------------------------------------------------------------------------------------- limits
84
- "one-token": { kind: "limit", what: "access", postgres: "roles, grants, row-level security", mason: "every user (mason user) and token reads every copied table: no per-user or per-tenant scope", wayOut: "copy only the tables every user may read" },
85
- "poll-full-scan": { kind: "limit", what: "keeping current by the poll (mason start without --replication)", postgres: "-", mason: "the xmin poll scans every added table whole, every poll (no index covers xmin): ~13–17 s for 13 GiB", wayOut: "logical replication: mason start --replication" },
86
- "writes": { kind: "limit", what: "INSERT, UPDATE, DELETE, DDL, a read-write transaction", postgres: "answers", mason: "refused by name: the copy is read-only (a read-write transaction: 25006)", wayOut: "write to the source database; the copy follows" },
87
- "numeric-over-38-digits": { kind: "limit", what: "a numeric of more than 38 digits, or holding NaN or Infinity: a column, or a constant ('1e400'::numeric) inside DuckDB's arithmetic or a WHERE", postgres: "exact", mason: "a double (15 digits), said in the log for a column; a constant the outermost SELECT answers is exact", wayOut: "numeric(p, s) with p ≤ 38" },
103
+ roles: { kind: "limit", what: "access", postgres: "roles, grants on tables and columns, row-level security", mason: "a role (mason/roles.yml) grants a user tables and columns, held on the analysed tree and refused as Postgres refuses them (42501 permission denied for table …); information_schema and mason mcp list a role what it may read, pg_catalog still lists every synced column; a role's where: or through: filters a table's rows by the user's attributes (mason user add --set), every read of it filtered on the analysed tree and held by Postgres row security in the catalog behind a security barrier as a Postgres policy is (a statement's own expressions run only on the rows the filter keeps; timing is not guaranteed), a whole row or TABLESAMPLE of a filtered table refused, so is a grouping set relying on its key for a column it does not group, EXPLAIN of a statement reading one said in Mason's lines alone with no engine plan and EXPLAIN ANALYZE of it refused (either would say what the engine knows of every row), a user missing a value refused (42501) at every statement; a user with no role reads as mason/roles.yml's default: role, nothing when the file names none, every synced table when there is no file; a token reads every synced table", wayOut: "mason user add <name> --role <role> --set <attribute>=<value>; name the filtered table's columns in place of its whole row; read the full plan as a user with no row filter", heldBy: "tests/duck-roles-wire.test.ts" },
104
+ "poll-full-scan": { kind: "limit", what: "keeping current by the poll (a table's sync: poll <interval>)", postgres: "-", mason: "the xmin poll scans its table whole, every poll (no index covers xmin): ~13–17 s for 13 GiB", wayOut: "logical replication for that table: sync: replication on its entry (mason add <table> --sync replication)" },
105
+ "writes": { kind: "limit", what: "INSERT, UPDATE, DELETE, DDL, a read-write transaction", postgres: "answers", mason: "refused by name: the synced data is read-only (a read-write transaction: 25006)", wayOut: "write to the source database; the synced data follows" },
106
+ "numeric-over-38-digits": { kind: "limit", what: "a numeric of more than 38 digits, or holding NaN or Infinity: a column, or a constant ('1e400'::numeric) inside Mason's arithmetic or a WHERE", postgres: "exact", mason: "a double (15 digits), said in the log for a column; a constant the outermost SELECT answers is exact", wayOut: "numeric(p, s) with p ≤ 38" },
88
107
  "numeric-array": { kind: "limit", what: "an unconstrained numeric[] column", postgres: "exact elements", mason: "double precision elements (15 digits), said in the log", wayOut: "numeric(p, s)[]" },
89
108
  "numeric-parameter": { kind: "limit", what: "a numeric parameter ($1) compared in a WHERE", postgres: "exact", mason: "a double (exact to 15 digits)", wayOut: "a literal" },
109
+ "spill-limit": { kind: "limit", what: "a statement that spills more to disk than Mason keeps for statements: a sort, a join or an aggregate past the memory limit", postgres: "spills up to temp_file_limit, or until the disk is full", mason: "fails alone with 53400 past half of the disk's room, what is free and what statements have spilled (or MASON_SPILL_LIMIT), so the sync keeps room to write; a disk that is really full is 53100", wayOut: "ask for less at once (a filter, a LIMIT, fewer or narrower columns), free disk, or raise MASON_SPILL_LIMIT" },
90
110
  "log-literals": { kind: "limit", what: ".mason/mason.log's statement lines", postgres: "-", mason: "each statement DuckDB ran, whole, literals included", wayOut: "keep the log private" },
91
111
  };
92
112
  /** the words a runtime refusal carries for the oracle and a reader: `Mason gap <id>` */
93
113
  export const gapMark = (id) => `Mason gap ${id}`;
94
114
  export const GAP_MARK = /\bMason gap ([a-z0-9-]+)/;
115
+ /** The entry a runtime refusal's TEXT names (`gapMark`), or null: only for a refusal raised inside DuckDB (a Mason
116
+ * function's error, src/duck/functions.ts), which crosses DuckDB as its message alone (src/duck/store.ts pgErrorOf);
117
+ * a refusal built outside it carries its id on the error (src/duck/statement.ts gapRefusal). */
118
+ export function gapIn(text) {
119
+ const id = GAP_MARK.exec(text)?.[1];
120
+ return id !== undefined && Object.hasOwn(GAPS, id) ? id : null;
121
+ }