sequel-notion 0.2.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +207 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +470 -0
  5. data/lib/sequel/adapters/notion.rb +159 -0
  6. data/lib/sequel/notion/dataset.rb +167 -0
  7. data/lib/sequel/notion/dataset_aggregates.rb +82 -0
  8. data/lib/sequel/notion/dataset_compounds.rb +89 -0
  9. data/lib/sequel/notion/dataset_computed.rb +124 -0
  10. data/lib/sequel/notion/dataset_grouping.rb +145 -0
  11. data/lib/sequel/notion/dataset_having.rb +99 -0
  12. data/lib/sequel/notion/dataset_joins.rb +124 -0
  13. data/lib/sequel/notion/dataset_pages.rb +145 -0
  14. data/lib/sequel/notion/dataset_selection.rb +51 -0
  15. data/lib/sequel/notion/dataset_truncation.rb +39 -0
  16. data/lib/sequel/notion/discovery.rb +76 -0
  17. data/lib/sequel/notion/errors.rb +9 -0
  18. data/lib/sequel/notion/filter_comparison.rb +123 -0
  19. data/lib/sequel/notion/filter_compiler.rb +121 -0
  20. data/lib/sequel/notion/filter_constants.rb +37 -0
  21. data/lib/sequel/notion/filter_helpers.rb +162 -0
  22. data/lib/sequel/notion/filter_like.rb +143 -0
  23. data/lib/sequel/notion/filter_like_tokenizer.rb +42 -0
  24. data/lib/sequel/notion/filter_negation.rb +76 -0
  25. data/lib/sequel/notion/filter_nested.rb +96 -0
  26. data/lib/sequel/notion/filter_nulls.rb +53 -0
  27. data/lib/sequel/notion/filter_predicates.rb +160 -0
  28. data/lib/sequel/notion/filter_shape.rb +75 -0
  29. data/lib/sequel/notion/filter_tables.rb +111 -0
  30. data/lib/sequel/notion/group_accumulator.rb +55 -0
  31. data/lib/sequel/notion/join_output.rb +58 -0
  32. data/lib/sequel/notion/join_where.rb +111 -0
  33. data/lib/sequel/notion/model_support.rb +52 -0
  34. data/lib/sequel/notion/notion_file.rb +228 -0
  35. data/lib/sequel/notion/page_api.rb +49 -0
  36. data/lib/sequel/notion/registry.rb +150 -0
  37. data/lib/sequel/notion/request_budget.rb +64 -0
  38. data/lib/sequel/notion/schema.rb +74 -0
  39. data/lib/sequel/notion/schema_lookup.rb +82 -0
  40. data/lib/sequel/notion/sort_compiler.rb +63 -0
  41. data/lib/sequel/notion/type_map.rb +125 -0
  42. data/lib/sequel/notion/type_map_builders.rb +125 -0
  43. data/lib/sequel/notion/type_map_dates.rb +67 -0
  44. data/lib/sequel/notion/type_map_extractors.rb +89 -0
  45. data/lib/sequel/notion/version.rb +7 -0
  46. data/lib/sequel/notion.rb +5 -0
  47. metadata +132 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a7f11227df00a9112d8b47f760985a0290807b89d7224d9e9fce4c93df7d7c9
4
+ data.tar.gz: 30ce794fb6a0d9fe33ac45790319539f8d0d63b2d0e8df5cce10367a566ad39a
5
+ SHA512:
6
+ metadata.gz: ccc702b83af21366469e911884ba787dc56d6bc94c6e186a7cf495f695c773f9830d4f98a53ed061f6e2f95ef2078be9f4a6554768fec336cece00179d3d363a
7
+ data.tar.gz: 27003c0414d36f7e0088eddafb1e4dfc1a8dc568bf121bc259685097565e66fb9029def065ac11afeaeca3ccea120b824e0467538191d697a24e36c4d075bb95
data/CHANGELOG.md ADDED
@@ -0,0 +1,207 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-10-10
4
+
5
+ ### Added
6
+
7
+ - Reads every data source property type (formulas, rollups and the
8
+ Notion unique ID included) and writes every writable one (title,
9
+ status, select, relation, people, dates and date ranges, files);
10
+ `nil` clears a property.
11
+ - Filters: `IS TRUE/FALSE`, `IN`, `LIKE` patterns, formula and rollup
12
+ properties, date `!=`, `Sequel[:col]` and qualified columns, bare
13
+ checkbox and formula columns, the Notion unique ID (`where(ID:
14
+ "TK-62")`, `ID > 3`, `order(:ID)`, checked live).
15
+ - Filters nested deeper than Notion's two levels are merged or
16
+ distributed into clauses, so `where(a).exclude(b: 1, c: 2)` and
17
+ `where(a).where((b & c) | d)` work; what cannot fit raises instead of
18
+ a 400.
19
+ - `where(id: …)` fetches pages directly; `Sequel::Model` works through
20
+ Sequel's cached loaders and explicit primary keys; computed
21
+ properties are `generated` for `skip_saving_columns`.
22
+ - 429 and 5xx responses are retried, honouring `Retry-After`; failures
23
+ raise `Sequel::DatabaseError`; requests are logged.
24
+ - Trashed pages are reachable by id and can be restored; discovery, the
25
+ search fallback and `register_all_data_sources` skip data sources in
26
+ the trash, which Notion's search still lists.
27
+ - `table_exists?`, the pagination extension and `Sequel::Rollback`
28
+ work. `paged_each` streams through Notion's cursor, needing no order
29
+ and one request per page, instead of OFFSET pages that re-read every
30
+ page before them.
31
+ - A relation Notion lists only 25 of (flagged `has_more`), or 25
32
+ people, is read in full from the page property endpoint, for the
33
+ columns a query returns (checked live with 26 relations).
34
+ - A rollup reads back as its value (a number, a date, or an `Array` of
35
+ the rolled-up values) instead of a raw hash (checked live); a rollup
36
+ whose function gives one value filters and sorts like one: `where(Sum:
37
+ 7)`, `Sum > 1`, `exclude`, `IN` (checked live for a sum and a
38
+ `latest_date`). `IN` on a formula works too, and a formula date `!=`
39
+ is `before or after` instead of raising. `where(F: nil)` on a formula
40
+ or a rollup is `is_empty`, since Notion checks a formula's emptiness
41
+ whatever its result type (checked live on string and number formulas);
42
+ formula negations need no guard, since Notion already excludes an
43
+ empty formula result (checked live).
44
+ - `sum`, `avg`, `min`, `max`, `count(:col)`, `distinct` and
45
+ `distinct(:col)` (`DISTINCT ON`) work, computed in Ruby over the rows
46
+ the query returns, with SQL's rules for `nil` and for `distinct`
47
+ before `limit` (checked live).
48
+ - `group`, `group_and_count` and `select_group` work with `count`,
49
+ `sum`, `avg`, `min` and `max`, computed in Ruby with one running value
50
+ per group; `order`, `offset` and `limit` apply to the groups; `having`
51
+ filters the groups, with SQL's three-valued logic (checked live).
52
+ - `union` (and `all:`), `intersect` and `except` work, computed in Ruby
53
+ (checked live); a `select` of columns on the result applies to the
54
+ combined rows.
55
+ - `join` and `left_join` on one equality, matched in Ruby: a relation
56
+ matches the pages it lists, each `where` condition runs in Notion on
57
+ the one table it tests, and aggregates and groups keep a qualified
58
+ column's table (checked live through a relation). A `where` on a left
59
+ join's joined table drops a row whose partners all fail it, as SQL
60
+ does, and keeps a row with no partner only if an empty page passes it
61
+ (checked live).
62
+
63
+ ### Changed
64
+
65
+ - Ruby 3.4 is required.
66
+ - Files live under `lib/sequel/notion/`, as RubyGems' naming guide asks
67
+ of a dashed gem name, and `require "sequel/notion"` (or Bundler's
68
+ auto-require) loads the adapter.
69
+ - Negations follow SQL: `exclude(N: 1)`, `NOT LIKE` and `NOT IN` no
70
+ longer match pages where the property is empty, as Notion's
71
+ `does_not_equal` and `does_not_contain` did, and as the date `!=`
72
+ already did not (checked live).
73
+ - A model's `save` of a loaded record sends only the changed columns,
74
+ instead of every column, which used to write back what a row reads
75
+ (a date's start only, the first 25 relations or people, rich text as
76
+ plain text), dropping a range's end, the other relations and the
77
+ formatting.
78
+ - `Task.last` needs an explicit order (a model adds no primary key
79
+ order, so it raises Sequel's `No order specified` instead of "Notion
80
+ cannot sort by id"); `order(:id)` and `order(:in_trash)` raise
81
+ `Sequel::Error` instead of sending a sort Notion rejects with a 400.
82
+ - Operations Notion does not compute itself (joins, groups, aggregates,
83
+ `distinct`, unions and the like) are computed in Ruby, only on a
84
+ dataset that opts in with `client_side`, which `max_requests:` can
85
+ cap, counting the requests Notion answers with a 200 (a rate-limited
86
+ attempt or a failed request counts nothing); without it they raise
87
+ before any request (checked live).
88
+
89
+ ### Fixed
90
+
91
+ - `offset`, `count`, `empty?`, `get` and `select_map`.
92
+ - Table names keep non-Latin letters (`"タスク"` → `:タスク`) instead of
93
+ normalising them all to the same empty name, which made a name lookup
94
+ pick another data source and broke `register_all_data_sources`. A
95
+ title with no letter or digit is named by its id, and a search that
96
+ matches two data sources raises.
97
+ - An exclusive date `Range` ends on the day before; an exclusive `Time`
98
+ range raises; a beginless `Range`, or a date `Hash` with no `:start`,
99
+ raises `Sequel::Error` naming the property, instead of Notion's 400
100
+ on a null start (checked live for the `Hash` case).
101
+ - A property named `id` or `in_trash` raises instead of overwriting the
102
+ page's own column.
103
+ - `where(id: [a, a])` gives one row, and an id matches itself written
104
+ with or without dashes.
105
+ - With `auto_register`, a lookup or `tables` made while another thread
106
+ is discovering runs its own discovery and gets a complete answer,
107
+ instead of reading a half-filled registry. Two data sources sharing a
108
+ name no longer break every lookup: only that name raises, until
109
+ `register_data_source` picks one. `register_data_source` keeps the id
110
+ as a String, so registering the same id as a Symbol and as a
111
+ String no longer raises, and discovery never overwrites it.
112
+ - Text is split into runs of 2000 UTF-16 units, the length Notion
113
+ checks, so a long text with emoji is no longer rejected.
114
+ - NaN and Infinity, in a write or a filter, a numeric string that
115
+ overflows to Infinity (`"1e400"`), and an external file URL that does
116
+ not parse raise `Sequel::Error` instead of a JSON or URI error; a
117
+ write names the property. A file URL with no path is named by the URL
118
+ rather than `"/"` or `""`. A number given as a String must be
119
+ decimal, in writes and filters: `"0x1A"` and `"1_000"` raise instead
120
+ of becoming 26 and 1000.
121
+ - `where(F: true)` and `exclude(F: false)` on a checkbox formula filter
122
+ through the formula's `checkbox` key instead of raising; a bare
123
+ formula column, `where(:F)` or `exclude(:F)`, filters on its checkbox
124
+ result instead of raising.
125
+ - `LIKE` with a wildcard on a multi-select, people or relation property
126
+ raises: Notion's `contains` there matches a whole value, so `"%ruby%"`
127
+ silently matched only the option `ruby`. `LIKE '%'` (wildcards only)
128
+ is `is_not_empty`, and `NOT LIKE '%'` `is_empty`, instead of `contains
129
+ ""` or an error.
130
+ - A `nil` in a list, `where(P: [a, nil])`, means empty, as
131
+ `where(P: nil)` does; it used to be sent as `""`, which Notion rejects
132
+ for a date.
133
+ - `order(..., nulls: :first)` raises instead of being dropped; Notion
134
+ always sorts empty values last.
135
+ - `update` with SQL (`Sequel.lit`) raises `Sequel::Error` instead of a
136
+ `NoMethodError`.
137
+ - A file of a type the adapter does not know reads back as a
138
+ `Sequel::Notion::File` keeping its `raw` hash, and is written back
139
+ unchanged, instead of making the whole page unreadable; two such files
140
+ compare by that hash, since they may have no URL. `File.new` refuses an
141
+ unknown keyword.
142
+ - `transaction(rollback: :always)` raises instead of keeping every write;
143
+ `rollback: :reraise` re-raises `Sequel::Rollback`.
144
+ - Upper-case page and data source ids are lower-cased, which Notion
145
+ requires, including an id given to `register_data_source`: it used to
146
+ make every query 404, `table_exists?` false and id lookups empty.
147
+ - A raw `NOT` over a date equality is `before or after` instead of a
148
+ `does_not_equal` Notion rejects.
149
+ - A date with an end reads back as a `Range` of the two ISO 8601
150
+ strings, which a write takes as is, instead of its start alone.
151
+ - A lock (`for_update`) on a join or a combined query raises, as on any
152
+ other query, instead of being dropped.
153
+ - `NOT IN` on a date, created-time or edited-time property is an `and`
154
+ of `before or after`, as `!=` is, instead of raising.
155
+ - `where(P: true)` on a property that is neither a checkbox nor a
156
+ formula raises, as `where(:P)` does, instead of sending a bare `true`
157
+ as a date, number or text.
158
+ - A `Hash` written to a multi-select, relation or people property
159
+ raises instead of becoming one option or id per key/value pair.
160
+ - `run`, `<<`, `truncate`, the `with_sql_*` writes and schema changes
161
+ raise `Sequel::Error` instead of a `NoMethodError` on a missing
162
+ `execute`; `order(Sequel.lit(...))` raises instead of sending the SQL
163
+ as a property name.
164
+ - A `union`, `intersect` or `except` names its columns after the first
165
+ query and matches the other's values by position, as SQL does: a
166
+ second query selecting other names used to keep them, so its rows
167
+ read `nil` under the first query's names, and `intersect` and
168
+ `except` missed equal rows. Queries selecting a different number of
169
+ columns raise.
170
+ - `refresh_schema!` refreshes every name of the data source, not only
171
+ the one given: an alias, or the id used as a table name, kept the old
172
+ columns, and a `select` of a new property through it raised.
173
+ - A date filter value that is not a `Date`, `Time`, `DateTime` or ISO
174
+ 8601 string raises instead of being sent as its `to_s`
175
+ (`where(Due: 123)` sent `"123"`).
176
+ - A `Complex` number, written or in a filter, raises `Sequel::Error`
177
+ instead of a `RangeError`.
178
+ - A negative unique ID in a filter (`where(ID: -5)`) raises, as a
179
+ negative `"-5"` already did.
180
+ - `where(P: [])` matches no page and sends no request, and
181
+ `exclude(P: [])` matches every page, as with Sequel's SQL adapters,
182
+ instead of raising; inside `and`, `or` and `NOT` they fold away.
183
+ - `where(id: [...]).order(...)` sorts the pages it fetches, as a query
184
+ would, instead of returning them in the list's order.
185
+ - Ordering rows computed in Ruby (groups, unions, joins) by a checkbox
186
+ sorts `false` first, as Notion does, instead of a `NoMethodError`;
187
+ values that cannot be compared raise `Sequel::Error`.
188
+ - A query run inside another's `each` block has its own `max_requests`
189
+ budget instead of spending the outer query's.
190
+ - `from_self` raises `Sequel::Error` instead of a `NoMethodError`.
191
+ - A data source fetch that `refresh_schema!` overtook no longer puts
192
+ the old properties back in the cache: it fetches again.
193
+ - A file named after its URL takes the path segment decoded (`a b.pdf`,
194
+ not `a%20b.pdf`); a segment that does not decode is kept as it is.
195
+ - A model keeps an Integer given to a number column (`rec.N = 5` is
196
+ `5`, as a read gives back) instead of Sequel's float typecast turning
197
+ it into `5.0`.
198
+ - Filtering on `id` or `in_trash` outside an id lookup
199
+ (`where(id: x).or(...)`) says they are the page's own columns instead
200
+ of "Unknown property"; `nil` on a unique ID says it is never empty.
201
+ - `exclude`, `NOT IN` and other negations on a rollup leave out pages
202
+ whose rollup is empty, as on other properties: Notion's rollup
203
+ `does_not_equal` matched an average over no relation (checked live).
204
+
205
+ ## 0.1.0
206
+
207
+ - First version.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stéphane D'Alu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.