Skip to content

Commit 3eb59b8

Browse files
authored
Implement sys::approximate_count() (#8692)
It takes a schema::ObjectType, so it is called like `sys::approximate_count(introspect Card)`. This is because the implementation works by querying the `reltuples` field of `pg_class`, and it really only makes sense for getting a full table. Having it take `anytype` and special casing would create weird discontinuities where the simple case worked like this and complex cases have a correctness or performance cliff. By default it includes all descendant types too; the ignore_subtypes parameter changes that. Fixes #4164.
1 parent f1c1ec8 commit 3eb59b8

File tree

6 files changed

+128
-1
lines changed

6 files changed

+128
-1
lines changed

docs/reference/stdlib/sys.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ System
1111
.. list-table::
1212
:class: funcoptable
1313

14+
* - :eql:func:`sys::approximate_count`
15+
- :eql:func-desc:`sys::approximate_count`
16+
1417
* - :eql:func:`sys::get_version`
1518
- :eql:func-desc:`sys::get_version`
1619

@@ -36,6 +39,43 @@ System
3639
----------
3740

3841

42+
.. eql:function:: sys::approximate_count( \
43+
type: schema::ObjectType, \
44+
NAMED ONLY ignore_subtypes: std::bool=false, \
45+
) -> int64
46+
47+
Return an approximate count of the number of objects belonging to
48+
a given type.
49+
50+
The ``type`` argument is a ``schema::ObjectType`` representing the
51+
type to query. It can be most easily obtained with the
52+
:eql:op:`introspect` operator.
53+
54+
By default, the count includes all subtypes of the provided type as well.
55+
If ``ignore_subtypes`` is true, then it includes only the type itself.
56+
57+
The value is based on postgres statistics, and may not be accurate.
58+
59+
.. code-block:: edgeql-repl
60+
61+
db> select sys::approximate_count(introspect schema::Type);
62+
{278}
63+
db> select sys::approximate_count(introspect schema::Type, ignore_subtypes:=True);
64+
{0}
65+
db> select schema::ObjectType {
66+
... name,
67+
... cnt := sys::approximate_count(schema::ObjectType, ignore_subtypes:=True),
68+
... };
69+
{
70+
schema::ObjectType {name: 'default::Issue', cnt: 4},
71+
schema::ObjectType {name: 'default::User', cnt: 2},
72+
...
73+
}
74+
75+
76+
----------
77+
78+
3979
.. eql:function:: sys::get_version() -> tuple<major: int64, \
4080
minor: int64, \
4181
stage: sys::VersionStage, \

edb/buildmeta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
# The merge conflict there is a nice reminder that you probably need
5858
# to write a patch in edb/pgsql/patches.py, and then you should preserve
5959
# the old value.
60-
EDGEDB_CATALOG_VERSION = 2025_05_09_00_00
60+
EDGEDB_CATALOG_VERSION = 2025_05_09_00_01
6161
EDGEDB_MAJOR_VERSION = 7
6262

6363

edb/lib/sys.edgeql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,15 @@ sys::__pg_or(a: OPTIONAL std::bool, b: OPTIONAL std::bool) -> std::bool
405405
SELECT a OR b;
406406
$$;
407407
};
408+
409+
410+
CREATE FUNCTION
411+
sys::approximate_count(
412+
type: schema::ObjectType,
413+
NAMED ONLY ignore_subtypes: std::bool=false,
414+
) -> int64
415+
{
416+
SET volatility := 'Stable';
417+
USING SQL FUNCTION 'edgedb.approximate_count';
418+
set impl_is_strict := false;
419+
};

edb/pgsql/delta.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,8 @@ def get_dummy_func_call(
15281528
param_type = param.get_type(schema)
15291529
pg_at = self.get_pgtype(cobj, param_type, schema)
15301530
args.append(f'NULL::{qt(pg_at)}')
1531+
if isinstance(param_type, s_objtypes.ObjectType):
1532+
args.append(f'NULL::uuid')
15311533

15321534
return f'{q(*name)}({", ".join(args)})'
15331535

edb/pgsql/metaschema.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,6 +1802,54 @@ def __init__(self) -> None:
18021802
)
18031803

18041804

1805+
# We create this version first (since it is used by the stdlib), and
1806+
# then replace it with the real version later.
1807+
class ApproximateCountDummy(trampoline.VersionedFunction):
1808+
text = '''
1809+
SELECT 0
1810+
'''
1811+
1812+
def __init__(self) -> None:
1813+
super().__init__(
1814+
name=('edgedb', 'approximate_count'),
1815+
args=[
1816+
('ignore_subtypes', ('bool',)),
1817+
('type', ('uuid',)),
1818+
('type_type', ('uuid',), "NULL"),
1819+
],
1820+
returns=('bigint',),
1821+
volatility='stable',
1822+
text=self.text,
1823+
)
1824+
1825+
1826+
class ApproximateCount(trampoline.VersionedFunction):
1827+
text = '''
1828+
SELECT coalesce(sum(reltuples::bigint), 0) AS estimate
1829+
FROM pg_class pc
1830+
WHERE pc.relname IN (
1831+
SELECT oa.source::text
1832+
FROM edgedb_VER."_SchemaObjectType__ancestors" oa
1833+
WHERE oa.target = type AND not ignore_subtypes
1834+
UNION
1835+
select type::text
1836+
) AND pc.reltuples >= 0;
1837+
'''
1838+
1839+
def __init__(self) -> None:
1840+
super().__init__(
1841+
name=('edgedb', 'approximate_count'),
1842+
args=[
1843+
('ignore_subtypes', ('bool',)),
1844+
('type', ('uuid',)),
1845+
('type_type', ('uuid',), "NULL"),
1846+
],
1847+
returns=('bigint',),
1848+
volatility='stable',
1849+
text=self.text,
1850+
)
1851+
1852+
18051853
class IssubclassFunction(trampoline.VersionedFunction):
18061854
text = '''
18071855
SELECT
@@ -5312,6 +5360,7 @@ def get_bootstrap_commands(
53125360
dbops.CreateFunction(FTSToRegconfig()),
53135361
dbops.CreateFunction(PadBase64StringFunction()),
53145362
dbops.CreateFunction(ResetQueryStatsFunction(False)),
5363+
dbops.CreateFunction(ApproximateCountDummy()),
53155364
]
53165365

53175366
non_trampolined = [
@@ -8709,6 +8758,7 @@ async def generate_support_functions(
87098758
dbops.CreateFunction(IssubclassFunction()),
87108759
dbops.CreateFunction(IssubclassFunction2()),
87118760
dbops.CreateFunction(GetSchemaObjectNameFunction()),
8761+
dbops.CreateFunction(ApproximateCount(), or_replace=True),
87128762
]
87138763
commands.add_commands(cmds)
87148764

tests/test_edgeql_functions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9406,3 +9406,26 @@ async def test_edgeql_functions_complex_types_04(self):
94069406
['https://edgedb.com', '~/screenshot.png'],
94079407
sort=True,
94089408
)
9409+
9410+
async def test_edgeql_functions_approximate_count(self):
9411+
await self.assert_query_result(
9412+
'''
9413+
select sys::approximate_count(introspect Issue);
9414+
''',
9415+
[int],
9416+
)
9417+
9418+
await self.assert_query_result(
9419+
'''
9420+
select sys::approximate_count(
9421+
introspect schema::Object, ignore_subtypes := True);
9422+
''',
9423+
[0],
9424+
)
9425+
9426+
val = await self.con.query_single(
9427+
'''
9428+
select sys::approximate_count(introspect schema::Object);
9429+
'''
9430+
)
9431+
self.assertGreater(val, 0)

0 commit comments

Comments
 (0)