From f5396fd4a7275e0427dbe9c569d4f2650bf38bc4 Mon Sep 17 00:00:00 2001 From: Fantix King Date: Thu, 21 Nov 2024 21:51:08 +0800 Subject: [PATCH] Add query performance statistics (#7869) Add edb_stat_statements Postgres extension (forked from the master branch of the upstream pg_stat_statement extension) to handle custom query performance statistics. `sys::QueryStats` is added as a view of the statistics. This is done in a way that, for each stats-significant SQL we send to the backend, one or more comment lines of "query stats info" JSONs are prepended for the Postgres extension to ingest and record in the modified statistics hash table. Among the stats info fields, `id: uuid` is especially important to identify different queries and accumulate stats of the same query onto the same hash table entry, which reflects some settings that affected the compilation (excluding the user schema version for common grouping of stats). Particularly, the first-8-bytes of `id` is also used by the Postgres extension to replace the underlying `queryId` of the SQL statement, so that the same frontend query can be recognized across all PARSE/EXECUTE operations in Postgres for stats recording. System queries, DDLs, and unrecognized queries are not recorded. Refs #7725 --- .editorconfig | 5 +- edb/buildmeta.py | 2 +- edb/lib/sys.edgeql | 143 + edb/pgsql/metaschema.py | 249 +- edb/pgsql/params.py | 10 + edb/server/compiler/compiler.py | 74 +- edb/server/compiler/dbstate.py | 1 + edb/server/compiler/sql.py | 55 + edb/server/config/__init__.py | 8 +- edb/server/config/ops.py | 23 +- edb/server/defines.py | 9 + edb/server/pgcluster.py | 13 + edb/server/pgcon/pgcon.pyx | 13 +- edb_stat_statements/.gitignore | 9 + edb_stat_statements/Makefile | 69 + .../edb_stat_statements--1.0.sql | 99 + edb_stat_statements/edb_stat_statements.c | 3297 +++++++++++++++++ .../edb_stat_statements.control | 5 + edb_stat_statements/expected/cleanup.out | 1 + edb_stat_statements/expected/cursors.out | 70 + edb_stat_statements/expected/dml.out.17 | 174 + edb_stat_statements/expected/dml.out.18 | 174 + .../expected/entry_timestamp.out | 159 + edb_stat_statements/expected/extended.out | 70 + .../expected/level_tracking.out.17 | 363 ++ .../expected/level_tracking.out.18 | 363 ++ .../expected/oldextversions.out | 96 + edb_stat_statements/expected/parallel.out.17 | 34 + edb_stat_statements/expected/parallel.out.18 | 34 + edb_stat_statements/expected/planning.out | 88 + edb_stat_statements/expected/privileges.out | 97 + edb_stat_statements/expected/select.out | 414 +++ .../expected/user_activity.out | 209 ++ edb_stat_statements/expected/utility.out.16 | 756 ++++ edb_stat_statements/expected/utility.out.17 | 745 ++++ edb_stat_statements/expected/wal.out.17 | 30 + edb_stat_statements/expected/wal.out.18 | 30 + edb_stat_statements/sql/cleanup.sql | 1 + edb_stat_statements/sql/cursors.sql | 30 + edb_stat_statements/sql/dml.sql | 95 + edb_stat_statements/sql/entry_timestamp.sql | 114 + edb_stat_statements/sql/extended.sql | 21 + edb_stat_statements/sql/level_tracking.sql | 173 + edb_stat_statements/sql/oldextversions.sql | 13 + edb_stat_statements/sql/parallel.sql | 26 + edb_stat_statements/sql/planning.sql | 31 + edb_stat_statements/sql/privileges.sql | 60 + edb_stat_statements/sql/select.sql | 149 + edb_stat_statements/sql/user_activity.sql | 67 + edb_stat_statements/sql/utility.sql | 374 ++ edb_stat_statements/sql/wal.sql | 20 + edb_stat_statements/t/010_restart.pl | 55 + setup.py | 70 +- tests/test_edgeql_sys.py | 106 +- tests/test_server_ops.py | 38 + 55 files changed, 9388 insertions(+), 46 deletions(-) create mode 100644 edb_stat_statements/.gitignore create mode 100644 edb_stat_statements/Makefile create mode 100644 edb_stat_statements/edb_stat_statements--1.0.sql create mode 100644 edb_stat_statements/edb_stat_statements.c create mode 100644 edb_stat_statements/edb_stat_statements.control create mode 100644 edb_stat_statements/expected/cleanup.out create mode 100644 edb_stat_statements/expected/cursors.out create mode 100644 edb_stat_statements/expected/dml.out.17 create mode 100644 edb_stat_statements/expected/dml.out.18 create mode 100644 edb_stat_statements/expected/entry_timestamp.out create mode 100644 edb_stat_statements/expected/extended.out create mode 100644 edb_stat_statements/expected/level_tracking.out.17 create mode 100644 edb_stat_statements/expected/level_tracking.out.18 create mode 100644 edb_stat_statements/expected/oldextversions.out create mode 100644 edb_stat_statements/expected/parallel.out.17 create mode 100644 edb_stat_statements/expected/parallel.out.18 create mode 100644 edb_stat_statements/expected/planning.out create mode 100644 edb_stat_statements/expected/privileges.out create mode 100644 edb_stat_statements/expected/select.out create mode 100644 edb_stat_statements/expected/user_activity.out create mode 100644 edb_stat_statements/expected/utility.out.16 create mode 100644 edb_stat_statements/expected/utility.out.17 create mode 100644 edb_stat_statements/expected/wal.out.17 create mode 100644 edb_stat_statements/expected/wal.out.18 create mode 100644 edb_stat_statements/sql/cleanup.sql create mode 100644 edb_stat_statements/sql/cursors.sql create mode 100644 edb_stat_statements/sql/dml.sql create mode 100644 edb_stat_statements/sql/entry_timestamp.sql create mode 100644 edb_stat_statements/sql/extended.sql create mode 100644 edb_stat_statements/sql/level_tracking.sql create mode 100644 edb_stat_statements/sql/oldextversions.sql create mode 100644 edb_stat_statements/sql/parallel.sql create mode 100644 edb_stat_statements/sql/planning.sql create mode 100644 edb_stat_statements/sql/privileges.sql create mode 100644 edb_stat_statements/sql/select.sql create mode 100644 edb_stat_statements/sql/user_activity.sql create mode 100644 edb_stat_statements/sql/utility.sql create mode 100644 edb_stat_statements/sql/wal.sql create mode 100644 edb_stat_statements/t/010_restart.pl diff --git a/.editorconfig b/.editorconfig index 562d6ce1ff4..3c044ad354a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,7 @@ indent_style = space indent_size = 2 indent_style = space -[ext/*.{c,cpp,h}] -indent_size = 4 +[edb_stat_statements/*.{c,h,l,y,pl,pm}] indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/edb/buildmeta.py b/edb/buildmeta.py index 54dd684c0c3..d381e336b4b 100644 --- a/edb/buildmeta.py +++ b/edb/buildmeta.py @@ -60,7 +60,7 @@ # The merge conflict there is a nice reminder that you probably need # to write a patch in edb/pgsql/patches.py, and then you should preserve # the old value. -EDGEDB_CATALOG_VERSION = 2024_11_12_01_00 +EDGEDB_CATALOG_VERSION = 2024_11_15_00_00 EDGEDB_MAJOR_VERSION = 6 diff --git a/edb/lib/sys.edgeql b/edb/lib/sys.edgeql index 9b3f4b9f5c7..c8ee834233e 100644 --- a/edb/lib/sys.edgeql +++ b/edb/lib/sys.edgeql @@ -28,6 +28,14 @@ CREATE SCALAR TYPE sys::VersionStage EXTENDING enum; +CREATE SCALAR TYPE sys::QueryType + EXTENDING enum; + + +CREATE SCALAR TYPE sys::OutputFormat + EXTENDING enum; + + CREATE ABSTRACT TYPE sys::SystemObject EXTENDING schema::Object; CREATE ABSTRACT TYPE sys::ExternalObject EXTENDING sys::SystemObject; @@ -86,6 +94,141 @@ ALTER TYPE sys::Role { }; +CREATE TYPE sys::QueryStats EXTENDING sys::ExternalObject { + CREATE LINK branch -> sys::Branch { + CREATE ANNOTATION std::description := + "The branch this statistics entry was collected in."; + }; + CREATE PROPERTY query -> std::str { + CREATE ANNOTATION std::description := + "Text string of a representative query."; + }; + CREATE PROPERTY query_type -> sys::QueryType { + CREATE ANNOTATION std::description := + "Type of the query."; + }; + + CREATE PROPERTY compilation_config -> std::json; + CREATE PROPERTY protocol_version -> tuple; + CREATE PROPERTY default_namespace -> std::str; + CREATE OPTIONAL PROPERTY namespace_aliases -> std::json; + CREATE OPTIONAL PROPERTY output_format -> sys::OutputFormat; + CREATE OPTIONAL PROPERTY expect_one -> std::bool; + CREATE OPTIONAL PROPERTY implicit_limit -> std::int64; + CREATE OPTIONAL PROPERTY inline_typeids -> std::bool; + CREATE OPTIONAL PROPERTY inline_typenames -> std::bool; + CREATE OPTIONAL PROPERTY inline_objectids -> std::bool; + + CREATE PROPERTY plans -> std::int64 { + CREATE ANNOTATION std::description := + "Number of times the query was planned in the backend."; + }; + CREATE PROPERTY total_plan_time -> std::duration { + CREATE ANNOTATION std::description := + "Total time spent planning the query in the backend."; + }; + CREATE PROPERTY min_plan_time -> std::duration { + CREATE ANNOTATION std::description := + "Minimum time spent planning the query in the backend. " + ++ "This field will be zero if the counter has been reset " + ++ "using the `sys::reset_query_stats` function " + ++ "with the `minmax_only` parameter set to `true` " + ++ "and never been planned since."; + }; + CREATE PROPERTY max_plan_time -> std::duration { + CREATE ANNOTATION std::description := + "Maximum time spent planning the query in the backend. " + ++ "This field will be zero if the counter has been reset " + ++ "using the `sys::reset_query_stats` function " + ++ "with the `minmax_only` parameter set to `true` " + ++ "and never been planned since."; + }; + CREATE PROPERTY mean_plan_time -> std::duration { + CREATE ANNOTATION std::description := + "Mean time spent planning the query in the backend."; + }; + CREATE PROPERTY stddev_plan_time -> std::duration { + CREATE ANNOTATION std::description := + "Population standard deviation of time spent " + ++ "planning the query in the backend."; + }; + + CREATE PROPERTY calls -> std::int64 { + CREATE ANNOTATION std::description := + "Number of times the query was executed."; + }; + CREATE PROPERTY total_exec_time -> std::duration { + CREATE ANNOTATION std::description := + "Total time spent executing the query in the backend."; + }; + CREATE PROPERTY min_exec_time -> std::duration { + CREATE ANNOTATION std::description := + "Minimum time spent executing the query in the backend, " + ++ "this field will be zero until this query is executed " + ++ "first time after reset performed by the " + ++ "`sys::reset_query_stats` function with the " + ++ "`minmax_only` parameter set to `true`"; + }; + CREATE PROPERTY max_exec_time -> std::duration { + CREATE ANNOTATION std::description := + "Maximum time spent executing the query in the backend, " + ++ "this field will be zero until this query is executed " + ++ "first time after reset performed by the " + ++ "`sys::reset_query_stats` function with the " + ++ "`minmax_only` parameter set to `true`"; + }; + CREATE PROPERTY mean_exec_time -> std::duration { + CREATE ANNOTATION std::description := + "Mean time spent executing the query in the backend."; + }; + CREATE PROPERTY stddev_exec_time -> std::duration { + CREATE ANNOTATION std::description := + "Population standard deviation of time spent " + ++ "executing the query in the backend."; + }; + + CREATE PROPERTY rows -> std::int64 { + CREATE ANNOTATION std::description := + "Total number of rows retrieved or affected by the query."; + }; + CREATE PROPERTY stats_since -> std::datetime { + CREATE ANNOTATION std::description := + "Time at which statistics gathering started for this query."; + }; + CREATE PROPERTY minmax_stats_since -> std::datetime { + CREATE ANNOTATION std::description := + "Time at which min/max statistics gathering started " + ++ "for this query (fields `min_plan_time`, `max_plan_time`, " + ++ "`min_exec_time` and `max_exec_time`)."; + }; +}; + + +CREATE FUNCTION +sys::reset_query_stats( + named only branch_name: OPTIONAL std::str = {}, + named only id: OPTIONAL std::uuid = {}, + named only minmax_only: OPTIONAL std::bool = false, +) -> OPTIONAL std::datetime { + CREATE ANNOTATION std::description := + 'Discard query statistics gathered so far corresponding to the ' + ++ 'specified `branch_name` and `id`. If either of the ' + ++ 'parameters is not specified, the statistics that match with the ' + ++ 'other parameter will be reset. If no parameter is specified, ' + ++ 'it will discard all statistics. When `minmax_only` is `true`, ' + ++ 'only the values of minimum and maximum planning and execution ' + ++ 'time will be reset (i.e. `min_plan_time`, `max_plan_time`, ' + ++ '`min_exec_time` and `max_exec_time` fields). The default value ' + ++ 'for `minmax_only` parameter is `false`. This function returns ' + ++ 'the time of a reset. This time is saved to `stats_reset` or ' + ++ '`minmax_stats_since` field of `sys::QueryStats` if the ' + ++ 'corresponding reset was actually performed.'; + SET volatility := 'Volatile'; + USING SQL FUNCTION 'edgedb.reset_query_stats'; +}; + + # An intermediate function is needed because we can't # cast JSON to tuples yet. DO NOT use directly, it'll go away. CREATE FUNCTION diff --git a/edb/pgsql/metaschema.py b/edb/pgsql/metaschema.py index 49144ca015c..ea7bb844c81 100644 --- a/edb/pgsql/metaschema.py +++ b/edb/pgsql/metaschema.py @@ -32,6 +32,7 @@ cast, ) +import json import re import edb._edgeql_parser as ql_parser @@ -57,6 +58,7 @@ from edb.schema import objtypes as s_objtypes from edb.schema import pointers as s_pointers from edb.schema import properties as s_props +from edb.schema import scalars as s_scalars from edb.schema import schema as s_schema from edb.schema import sources as s_sources from edb.schema import types as s_types @@ -4906,6 +4908,118 @@ def __init__(self) -> None: ) +class ResetQueryStatsFunction(trampoline.VersionedFunction): + text = r""" + DECLARE + tenant_id TEXT; + other_tenant_exists BOOLEAN; + db_oid OID; + queryid bigint; + BEGIN + tenant_id := edgedb_VER.get_backend_tenant_id(); + IF id IS NULL THEN + queryid := 0; + ELSE + queryid := edgedbext.edb_stat_queryid(id); + END IF; + + SELECT EXISTS ( + SELECT 1 + FROM + pg_database dat + CROSS JOIN LATERAL ( + SELECT + edgedb_VER.shobj_metadata(dat.oid, 'pg_database') + AS description + ) AS d + WHERE + (d.description)->>'id' IS NOT NULL + AND (d.description)->>'tenant_id' != tenant_id + ) INTO other_tenant_exists; + + IF branch_name IS NULL THEN + IF other_tenant_exists THEN + RETURN edgedbext.edb_stat_statements_reset( + 0, -- userid + ARRAY( + SELECT + dat.oid + FROM + pg_database dat + CROSS JOIN LATERAL ( + SELECT + edgedb_VER.shobj_metadata(dat.oid, + 'pg_database') + AS description + ) AS d + WHERE + (d.description)->>'id' IS NOT NULL + AND (d.description)->>'tenant_id' = tenant_id + ), + queryid, + COALESCE(minmax_only, false) + ); + ELSE + RETURN edgedbext.edb_stat_statements_reset( + 0, -- userid + '{}', -- database oid + queryid, + COALESCE(minmax_only, false) + ); + END IF; + ELSE + SELECT + dat.oid INTO db_oid + FROM + pg_database dat + CROSS JOIN LATERAL ( + SELECT + edgedb_VER.shobj_metadata(dat.oid, 'pg_database') + AS description + ) AS d + WHERE + (d.description)->>'id' IS NOT NULL + AND (d.description)->>'tenant_id' = tenant_id + AND edgedb_VER.get_database_frontend_name(dat.datname) = + branch_name; + + IF db_oid IS NULL THEN + RETURN NULL::edgedbt.timestamptz_t; + END IF; + + RETURN edgedbext.edb_stat_statements_reset( + 0, -- userid + ARRAY[db_oid], + queryid, + COALESCE(minmax_only, false) + ); + END IF; + + RETURN now()::edgedbt.timestamptz_t; + END; + """ + + noop_text = r""" + BEGIN + RETURN NULL::edgedbt.timestamptz_t; + END; + """ + + def __init__(self, enable_stats: bool) -> None: + super().__init__( + name=('edgedb', 'reset_query_stats'), + args=[ + ('branch_name', ('text',)), + ('id', ('uuid',)), + ('minmax_only', ('bool',)), + ], + returns=('edgedbt', 'timestamptz_t'), + volatility='volatile', + language='plpgsql', + text=self.text if enable_stats else self.noop_text, + ) + + def _maybe_trampoline( cmd: dbops.Command, out: list[trampoline.Trampoline] ) -> None: @@ -5131,6 +5245,7 @@ def get_bootstrap_commands( dbops.CreateFunction(FTSNormalizeDocFunction()), dbops.CreateFunction(FTSToRegconfig()), dbops.CreateFunction(PadBase64StringFunction()), + dbops.CreateFunction(ResetQueryStatsFunction(False)), ] commands = dbops.CommandGroup() @@ -5152,15 +5267,19 @@ async def create_pg_extensions( commands.add_command( dbops.CreateSchema(name=ext_schema, conditional=True), ) - if ( - inst_params.existing_exts is None - or inst_params.existing_exts.get("uuid-ossp") is None - ): - commands.add_commands([ - dbops.CreateExtension( - dbops.Extension(name='uuid-ossp', schema=ext_schema), - ), - ]) + extensions = ["uuid-ossp"] + if backend_params.has_stat_statements: + extensions.append("edb_stat_statements") + for ext in extensions: + if ( + inst_params.existing_exts is None + or inst_params.existing_exts.get(ext) is None + ): + commands.add_commands([ + dbops.CreateExtension( + dbops.Extension(name=ext, schema=ext_schema), + ), + ]) block = dbops.PLTopBlock() commands.generate(block) await _execute_block(conn, block) @@ -5857,6 +5976,109 @@ def _generate_schema_ver_views(schema: s_schema.Schema) -> List[dbops.View]: return views +def _generate_stats_views(schema: s_schema.Schema) -> List[dbops.View]: + QueryStats = schema.get( + 'sys::QueryStats', + type=s_objtypes.ObjectType, + ) + pvd = common.get_backend_name( + schema, + QueryStats + .getptr(schema, s_name.UnqualName("protocol_version")) + .get_target(schema) # type: ignore + ) + QueryType = schema.get( + 'sys::QueryType', + type=s_scalars.ScalarType, + ) + query_type_domain = common.get_backend_name(schema, QueryType) + type_mapping = { + str(v): k for k, v in defines.QueryType.__members__.items() + } + output_format_domain = common.get_backend_name( + schema, schema.get('sys::OutputFormat', type=s_scalars.ScalarType) + ) + + def float64_to_duration_t(val: str) -> str: + return f"({val} * interval '1ms')::edgedbt.duration_t" + + query_stats_fields = { + 'id': "s.id", + 'name': "s.id::text", + 'name__internal': "s.queryid::text", + 'builtin': "false", + 'internal': "false", + 'computed_fields': 'ARRAY[]::text[]', + + 'compilation_config': "s.extras->'cc'", + 'protocol_version': f"ROW(s.extras->'pv'->0, s.extras->'pv'->1)::{pvd}", + 'default_namespace': "s.extras->>'dn'", + 'namespace_aliases': "s.extras->'na'", + 'output_format': f"(s.extras->>'of')::{output_format_domain}", + 'expect_one': "(s.extras->'e1')::boolean", + 'implicit_limit': "(s.extras->'il')::bigint", + 'inline_typeids': "(s.extras->'ii')::boolean", + 'inline_typenames': "(s.extras->'in')::boolean", + 'inline_objectids': "(s.extras->'io')::boolean", + + 'branch': "((d.description)->>'id')::uuid", + 'query': "s.query", + 'query_type': f"(t.mapping->>s.stmt_type::text)::{query_type_domain}", + + 'plans': 's.plans', + 'total_plan_time': float64_to_duration_t('s.total_plan_time'), + 'min_plan_time': float64_to_duration_t('s.min_plan_time'), + 'max_plan_time': float64_to_duration_t('s.max_plan_time'), + 'mean_plan_time': float64_to_duration_t('s.mean_plan_time'), + 'stddev_plan_time': float64_to_duration_t('s.stddev_plan_time'), + + 'calls': 's.calls', + 'total_exec_time': float64_to_duration_t('s.total_exec_time'), + 'min_exec_time': float64_to_duration_t('s.min_exec_time'), + 'max_exec_time': float64_to_duration_t('s.max_exec_time'), + 'mean_exec_time': float64_to_duration_t('s.mean_exec_time'), + 'stddev_exec_time': float64_to_duration_t('s.stddev_exec_time'), + + 'rows': 's.rows', + 'stats_since': 's.stats_since::edgedbt.timestamptz_t', + 'minmax_stats_since': 's.minmax_stats_since::edgedbt.timestamptz_t', + } + + query_stats_query = fr''' + SELECT + {format_fields(schema, QueryStats, query_stats_fields)} + FROM + edgedbext.edb_stat_statements AS s + INNER JOIN pg_database dat ON s.dbid = dat.oid + CROSS JOIN LATERAL ( + SELECT + edgedb_VER.shobj_metadata(dat.oid, 'pg_database') + AS description + ) AS d + CROSS JOIN LATERAL ( + SELECT {ql(json.dumps(type_mapping))}::jsonb AS mapping + ) AS t + WHERE + s.id IS NOT NULL + AND (d.description)->>'id' IS NOT NULL + AND (d.description)->>'tenant_id' + = edgedb_VER.get_backend_tenant_id() + AND t.mapping ? s.stmt_type::text + ''' + + objects = { + QueryStats: query_stats_query, + } + + views: list[dbops.View] = [] + for obj, query in objects.items(): + tabview = trampoline.VersionedView( + name=tabname(schema, obj), query=query) + views.append(tabview) + + return views + + def _make_json_caster( schema: s_schema.Schema, stype: s_types.Type, @@ -7686,6 +7908,15 @@ def get_synthetic_type_views( for verview in _generate_schema_ver_views(schema): commands.add_command(dbops.CreateView(verview, or_replace=True)) + if backend_params.has_stat_statements: + for stats_view in _generate_stats_views(schema): + commands.add_command(dbops.CreateView(stats_view, or_replace=True)) + commands.add_command( + dbops.CreateFunction( + ResetQueryStatsFunction(True), or_replace=True + ) + ) + return commands diff --git a/edb/pgsql/params.py b/edb/pgsql/params.py index a4df60054a4..7d9d64097fc 100644 --- a/edb/pgsql/params.py +++ b/edb/pgsql/params.py @@ -40,6 +40,8 @@ class BackendCapabilities(enum.IntFlag): CREATE_ROLE = 1 << 3 #: Whether CREATE DATABASE is allowed CREATE_DATABASE = 1 << 4 + #: Whether extension "edb_stat_statements" is available + STAT_STATEMENTS = 1 << 5 ALL_BACKEND_CAPABILITIES = ( @@ -48,6 +50,7 @@ class BackendCapabilities(enum.IntFlag): | BackendCapabilities.C_UTF8_LOCALE | BackendCapabilities.CREATE_ROLE | BackendCapabilities.CREATE_DATABASE + | BackendCapabilities.STAT_STATEMENTS ) @@ -111,6 +114,13 @@ def has_create_database(self) -> bool: & BackendCapabilities.CREATE_DATABASE ) + @property + def has_stat_statements(self) -> bool: + return self.has_superuser_access and bool( + self.instance_params.capabilities + & BackendCapabilities.STAT_STATEMENTS + ) + @functools.lru_cache def get_default_runtime_params( diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py index 906e65a1eae..b5bd9234341 100644 --- a/edb/server/compiler/compiler.py +++ b/edb/server/compiler/compiler.py @@ -60,6 +60,7 @@ from edb.common import uuidgen from edb.edgeql import ast as qlast +from edb.edgeql import codegen as qlcodegen from edb.edgeql import compiler as qlcompiler from edb.edgeql import qltypes @@ -566,6 +567,8 @@ def compile_sql( allow_user_specified_id=allow_user_specified_id, apply_access_policies_sql=apply_access_policies_sql, disambiguate_column_names=False, + backend_runtime_params=self.state.backend_runtime_params, + protocol_version=(-3, 0), # emulated PG binary protocol version ) def compile_serialized_request( @@ -1641,6 +1644,52 @@ def _compile_ql_query( is_explain = explain_data is not None current_tx = ctx.state.current_tx() + sql_info: Dict[str, Any] = {} + if ( + not ctx.bootstrap_mode + and ctx.backend_runtime_params.has_stat_statements + and not ctx.schema_reflection_mode + ): + spec = ctx.compiler_state.config_spec + cconfig = config.to_json_obj( + spec, + { + **current_tx.get_system_config(), + **current_tx.get_database_config(), + **current_tx.get_session_config(), + }, + setting_filter=lambda v: v.name in spec + and spec[v.name].affects_compilation, + include_source=False, + ) + extras: Dict[str, Any] = { + 'cc': dict(sorted(cconfig.items())), # compilation_config + 'pv': ctx.protocol_version, # protocol_version + 'of': ctx.output_format, # output_format + 'e1': ctx.expected_cardinality_one, # expect_one + 'il': ctx.implicit_limit, # implicit_limit + 'ii': ctx.inline_typeids, # inline_typeids + 'in': ctx.inline_typenames, # inline_typenames + 'io': ctx.inline_objectids, # inline_objectids + } + modaliases = dict(current_tx.get_modaliases()) + # dn: default_namespace + extras['dn'] = modaliases.pop(None, defines.DEFAULT_MODULE_ALIAS) + if modaliases: + # na: namespace_aliases + extras['na'] = dict(sorted(modaliases.items())) + + sql_info.update({ + 'query': qlcodegen.generate_source(ql), + 'type': defines.QueryType.EdgeQL, + 'extras': json.dumps(extras), + }) + id_hash = hashlib.blake2b(digest_size=16) + id_hash.update( + json.dumps(sql_info).encode(defines.EDGEDB_ENCODING) + ) + sql_info['id'] = str(uuidgen.from_bytes(id_hash.digest())) + base_schema = ( ctx.compiler_state.std_schema if not _get_config_val(ctx, '__internal_query_reflschema') @@ -1706,12 +1755,11 @@ def _compile_ql_query( # If requested, embed the EdgeQL text in the SQL. if debug.flags.edgeql_text_in_sql and source: - sql_debug_obj = dict(edgeql=source.text()) - sql_debug_prefix = '-- ' + json.dumps(sql_debug_obj) + '\n' - sql_text = sql_debug_prefix + sql_text - if func_call_sql is not None: - func_call_sql = sql_debug_prefix + func_call_sql - sql_bytes = sql_text.encode(defines.EDGEDB_ENCODING) + sql_info['edgeql'] = source.text() + if sql_info: + sql_info_prefix = '-- ' + json.dumps(sql_info) + '\n' + else: + sql_info_prefix = '' globals = None if ir.globals: @@ -1743,21 +1791,23 @@ def _compile_ql_query( ) sql_hash = _hash_sql( - sql_bytes, + sql_text.encode(defines.EDGEDB_ENCODING), mode=str(ctx.output_format).encode(), intype=in_type_id.bytes, outtype=out_type_id.bytes) cache_func_call = None if func_call_sql is not None: - func_call_sql_bytes = func_call_sql.encode(defines.EDGEDB_ENCODING) func_call_sql_hash = _hash_sql( - func_call_sql_bytes, + func_call_sql.encode(defines.EDGEDB_ENCODING), mode=str(ctx.output_format).encode(), intype=in_type_id.bytes, outtype=out_type_id.bytes, ) - cache_func_call = (func_call_sql_bytes, func_call_sql_hash) + cache_func_call = ( + (sql_info_prefix + func_call_sql).encode(defines.EDGEDB_ENCODING), + func_call_sql_hash, + ) if is_explain: if isinstance(ir.schema, s_schema.ChainedSchema): @@ -1772,7 +1822,7 @@ def _compile_ql_query( query_asts = None return dbstate.Query( - sql=sql_bytes, + sql=(sql_info_prefix + sql_text).encode(defines.EDGEDB_ENCODING), sql_hash=sql_hash, cache_sql=cache_sql, cache_func_call=cache_func_call, @@ -2468,6 +2518,8 @@ def compile_sql_as_unit_group( include_edgeql_io_format_alternative=True, allow_prepared_statements=False, disambiguate_column_names=True, + backend_runtime_params=ctx.backend_runtime_params, + protocol_version=ctx.protocol_version, ) qug = dbstate.QueryUnitGroup( diff --git a/edb/server/compiler/dbstate.py b/edb/server/compiler/dbstate.py index c442dc9dbe2..89cc42e2506 100644 --- a/edb/server/compiler/dbstate.py +++ b/edb/server/compiler/dbstate.py @@ -516,6 +516,7 @@ class SQLQueryUnit: query: str = dataclasses.field(repr=False) """Translated query text.""" + prefix_len: int = 0 translation_data: Optional[pgcodegen.TranslationData] = None """Translation source map.""" diff --git a/edb/server/compiler/sql.py b/edb/server/compiler/sql.py index 0371874be53..1f0ce8a52e3 100644 --- a/edb/server/compiler/sql.py +++ b/edb/server/compiler/sql.py @@ -24,14 +24,18 @@ import functools import hashlib import immutables +import json from edb import errors +from edb.common import uuidgen +from edb.server import defines from edb.schema import schema as s_schema from edb.pgsql import ast as pgast from edb.pgsql import common as pg_common from edb.pgsql import codegen as pg_codegen +from edb.pgsql import params as pg_params from edb.pgsql import parser as pg_parser from . import dbstate @@ -66,6 +70,8 @@ def compile_sql( include_edgeql_io_format_alternative: bool = False, allow_prepared_statements: bool = True, disambiguate_column_names: bool, + backend_runtime_params: pg_params.BackendRuntimeParams, + protocol_version: defines.ProtocolVersion, ) -> List[dbstate.SQLQueryUnit]: opts = ResolverOptionsPartial( query_str=query_str, @@ -84,6 +90,7 @@ def compile_sql( for stmt in stmts: orig_text = pg_codegen.generate_source(stmt) fe_settings = tx_state.current_fe_settings() + track_stats = False unit = dbstate.SQLQueryUnit( orig_query=orig_text, @@ -223,6 +230,7 @@ def compile_sql( translation_data=stmt_source.translation_data, ) unit.command_complete_tag = dbstate.TagPlain(tag=b"PREPARE") + track_stats = True elif isinstance(stmt, pgast.ExecuteStmt): if not allow_prepared_statements: @@ -245,6 +253,8 @@ def compile_sql( be_stmt_name=mangled_name.encode("utf-8"), ) unit.cardinality = enums.Cardinality.MANY + track_stats = True + elif isinstance(stmt, pgast.DeallocateStmt): if not allow_prepared_statements: raise errors.UnsupportedFeatureError( @@ -288,12 +298,57 @@ def compile_sql( unit.cardinality = enums.Cardinality.NO_RESULT else: unit.cardinality = enums.Cardinality.MANY + track_stats = True else: from edb.pgsql import resolver as pg_resolver pg_resolver.dispatch._raise_unsupported(stmt) unit.stmt_name = compute_stmt_name(unit.query, tx_state).encode("utf-8") + if track_stats and backend_runtime_params.has_stat_statements: + cconfig: dict[str, dbstate.SQLSetting] = { + k: v for k, v in fe_settings.items() + if k is not None and v is not None and k in FE_SETTINGS_MUTABLE + } + cconfig.pop('server_version', None) + cconfig.pop('server_version_num', None) + if allow_user_specified_id is not None: + cconfig.setdefault( + 'allow_user_specified_id', + ('true' if allow_user_specified_id else 'false',), + ) + if apply_access_policies_sql is not None: + cconfig.setdefault( + 'apply_access_policies_sql', + ('true' if apply_access_policies_sql else 'false',), + ) + search_path = parse_search_path(cconfig.pop("search_path", ("",))) + cconfig = dict(sorted((k, v) for k, v in cconfig.items())) + extras = { + 'cc': cconfig, # compilation_config + 'pv': protocol_version, # protocol_version + 'dn': ', '.join(search_path), # default_namespace + } + sql_info = { + 'query': orig_text, + 'type': defines.QueryType.SQL, + 'extras': json.dumps(extras), + } + id_hash = hashlib.blake2b(digest_size=16) + id_hash.update( + json.dumps(sql_info).encode(defines.EDGEDB_ENCODING) + ) + sql_info['id'] = str(uuidgen.from_bytes(id_hash.digest())) + prefix = ''.join([ + '-- ', + json.dumps(sql_info), + '\n', + ]) + unit.prefix_len = len(prefix) + unit.query = prefix + unit.query + if unit.eql_format_query is not None: + unit.eql_format_query = prefix + unit.eql_format_query + if isinstance(stmt, pgast.DMLQuery): unit.capabilities |= enums.Capability.MODIFICATIONS diff --git a/edb/server/config/__init__.py b/edb/server/config/__init__.py index af629499294..6ddd9fdd2a4 100644 --- a/edb/server/config/__init__.py +++ b/edb/server/config/__init__.py @@ -26,7 +26,9 @@ from edb.edgeql.qltypes import ConfigScope from .ops import OpCode, Operation, SettingValue -from .ops import spec_to_json, to_json, from_json, set_value, to_edgeql +from .ops import ( + spec_to_json, to_json_obj, to_json, from_json, set_value, to_edgeql +) from .ops import value_from_json, value_to_json_value, coerce_single_value from .spec import ( Spec, FlatSpec, ChainedSpec, Setting, @@ -40,8 +42,8 @@ __all__ = ( 'lookup', 'Spec', 'FlatSpec', 'ChainedSpec', 'Setting', 'SettingValue', - 'spec_to_json', 'to_json', 'to_edgeql', 'from_json', 'set_value', - 'value_from_json', 'value_to_json_value', + 'spec_to_json', 'to_json_obj', 'to_json', 'to_edgeql', 'from_json', + 'set_value', 'value_from_json', 'value_to_json_value', 'ConfigScope', 'OpCode', 'Operation', 'ConfigType', 'CompositeConfigType', 'load_spec_from_schema', 'load_ext_spec_from_schema', diff --git a/edb/server/config/ops.py b/edb/server/config/ops.py index 5244acba9c9..57194a5b52b 100644 --- a/edb/server/config/ops.py +++ b/edb/server/config/ops.py @@ -24,6 +24,7 @@ from typing import ( Any, Callable, + Dict, Optional, TypeVar, Union, @@ -429,17 +430,17 @@ def value_to_edgeql_const( return qlcodegen.generate_source(ql) -def to_json( +def to_json_obj( spec: spec.Spec, storage: Mapping[str, SettingValue], *, setting_filter: Optional[Callable[[SettingValue], bool]] = None, include_source: bool = True, -) -> str: +) -> Dict[str, Any]: dct = {} for name, value in storage.items(): - setting = spec[name] if setting_filter is None or setting_filter(value): + setting = spec[name] val = value_to_json_value(setting, value.value) if include_source: dct[name] = { @@ -450,6 +451,22 @@ def to_json( } else: dct[name] = val + return dct + + +def to_json( + spec: spec.Spec, + storage: Mapping[str, SettingValue], + *, + setting_filter: Optional[Callable[[SettingValue], bool]] = None, + include_source: bool = True, +) -> str: + dct = to_json_obj( + spec, + storage, + setting_filter=setting_filter, + include_source=include_source, + ) return json.dumps(dct) diff --git a/edb/server/defines.py b/edb/server/defines.py index a8e1d746d69..c612865cf1c 100644 --- a/edb/server/defines.py +++ b/edb/server/defines.py @@ -20,6 +20,8 @@ from __future__ import annotations from typing import TypeAlias +import enum + from edb import buildmeta from edb.common import enum as s_enum @@ -100,3 +102,10 @@ class TxIsolationLevel(s_enum.StrEnum): RepeatableRead = 'REPEATABLE READ' Serializable = 'SERIALIZABLE' + + +# Mapping to the backend `edb_stat_statements.stmt_type` values, +# as well as `sys::QueryType` in edb/lib/sys.edgeql +class QueryType(enum.IntEnum): + EdgeQL = 1 + SQL = 2 diff --git a/edb/server/pgcluster.py b/edb/server/pgcluster.py index ca3ab04c9ae..393b126bc4a 100644 --- a/edb/server/pgcluster.py +++ b/edb/server/pgcluster.py @@ -564,6 +564,12 @@ async def start( # `max_connections` scenarios. 'max_locks_per_transaction': 1024, 'max_pred_locks_per_transaction': 1024, + "shared_preload_libraries": ",".join( + [ + "edb_stat_statements", + ] + ), + "edb_stat_statements.track_planning": "true", } if os.getenv('EDGEDB_DEBUG_PGSERVER'): @@ -1168,6 +1174,13 @@ async def _detect_capabilities( if roles['rolcreatedb']: caps |= pgparams.BackendCapabilities.CREATE_DATABASE + stats_ver = await conn.sql_fetch_val(b""" + SELECT default_version FROM pg_available_extensions + WHERE name = 'edb_stat_statements'; + """) + if stats_ver in (b"1.0",): + caps |= pgparams.BackendCapabilities.STAT_STATEMENTS + return caps async def _get_pg_settings( diff --git a/edb/server/pgcon/pgcon.pyx b/edb/server/pgcon/pgcon.pyx index d4a7bf58317..7f1426d8300 100644 --- a/edb/server/pgcon/pgcon.pyx +++ b/edb/server/pgcon/pgcon.pyx @@ -2174,13 +2174,19 @@ cdef class PGConnection: while True: field_type = self.buffer.read_byte() if field_type == b'P': # Position - qu = (action.query_unit.translation_data - if action.query_unit else None) + if action.query_unit is None: + translation_data = None + offset = 0 + else: + qu = action.query_unit + translation_data = qu.translation_data + offset = -qu.prefix_len self._write_error_position( msg_buf, action.args[0], self.buffer.read_null_str(), - qu + translation_data, + offset, ) continue else: @@ -2226,6 +2232,7 @@ cdef class PGConnection: else: offset = 0 translation_data = qu.translation_data + offset -= qu.prefix_len else: query_text = b"" translation_data = None diff --git a/edb_stat_statements/.gitignore b/edb_stat_statements/.gitignore new file mode 100644 index 00000000000..0df4d7f493e --- /dev/null +++ b/edb_stat_statements/.gitignore @@ -0,0 +1,9 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ +/expected/dml.out +/expected/level_tracking.out +/expected/parallel.out +/expected/utility.out +/expected/wal.out diff --git a/edb_stat_statements/Makefile b/edb_stat_statements/Makefile new file mode 100644 index 00000000000..f53c59c36f3 --- /dev/null +++ b/edb_stat_statements/Makefile @@ -0,0 +1,69 @@ +MODULE_big = edb_stat_statements +OBJS = \ + $(WIN32RES) \ + edb_stat_statements.o + +EXTENSION = edb_stat_statements +DATA = edb_stat_statements--1.0.sql +PGFILEDESC = "edb_stat_statements - execution statistics of EdgeDB queries" + +LDFLAGS_SL += $(filter -lm, $(LIBS)) + +REGRESS = select dml cursors utility level_tracking planning \ + user_activity wal entry_timestamp privileges \ + parallel cleanup oldextversions + +TAP_TESTS = 1 +PG_MAJOR = $(shell $(PG_CONFIG) --version | grep -oE '[0-9]+' | head -1) + +ifeq ($(shell test $(PG_MAJOR) -ge 18 && echo true), true) + REGRESS += extended +endif + +all: + +expected/dml.out: + if [ $(PG_MAJOR) -ge 18 ]; then \ + cp expected/dml.out.18 expected/dml.out; \ + else \ + cp expected/dml.out.17 expected/dml.out; \ + fi + +expected/level_tracking.out: + if [ $(PG_MAJOR) -ge 18 ]; then \ + cp expected/level_tracking.out.18 expected/level_tracking.out; \ + else \ + cp expected/level_tracking.out.17 expected/level_tracking.out; \ + fi + +expected/parallel.out: + if [ $(PG_MAJOR) -ge 18 ]; then \ + cp expected/parallel.out.18 expected/parallel.out; \ + else \ + cp expected/parallel.out.17 expected/parallel.out; \ + fi + +expected/utility.out: + if [ $(PG_MAJOR) -ge 17 ]; then \ + cp expected/utility.out.17 expected/utility.out; \ + else \ + cp expected/utility.out.16 expected/utility.out; \ + fi + +expected/wal.out: + if [ $(PG_MAJOR) -ge 18 ]; then \ + cp expected/wal.out.18 expected/wal.out; \ + else \ + cp expected/wal.out.17 expected/wal.out; \ + fi + +installcheck: \ + expected/dml.out \ + expected/level_tracking.out \ + expected/parallel.out \ + expected/utility.out \ + expected/wal.out + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/edb_stat_statements/edb_stat_statements--1.0.sql b/edb_stat_statements/edb_stat_statements--1.0.sql new file mode 100644 index 00000000000..dd08d8045f1 --- /dev/null +++ b/edb_stat_statements/edb_stat_statements--1.0.sql @@ -0,0 +1,99 @@ +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION edb_stat_statements" to load this file. \quit + +-- Register functions. +CREATE FUNCTION edb_stat_statements_reset(IN userid Oid DEFAULT 0, + IN dbids Oid[] DEFAULT '{}', + IN queryid bigint DEFAULT 0, + IN minmax_only boolean DEFAULT false +) +RETURNS timestamp with time zone +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL SAFE; + +CREATE FUNCTION edb_stat_queryid(IN id uuid) +RETURNS bigint +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL SAFE; + +CREATE FUNCTION edb_stat_statements(IN showtext boolean, + OUT userid oid, + OUT dbid oid, + OUT toplevel bool, + OUT queryid bigint, + OUT query text, + OUT extras jsonb, + OUT id uuid, + OUT stmt_type int2, + OUT plans int8, + OUT total_plan_time float8, + OUT min_plan_time float8, + OUT max_plan_time float8, + OUT mean_plan_time float8, + OUT stddev_plan_time float8, + OUT calls int8, + OUT total_exec_time float8, + OUT min_exec_time float8, + OUT max_exec_time float8, + OUT mean_exec_time float8, + OUT stddev_exec_time float8, + OUT rows int8, + OUT shared_blks_hit int8, + OUT shared_blks_read int8, + OUT shared_blks_dirtied int8, + OUT shared_blks_written int8, + OUT local_blks_hit int8, + OUT local_blks_read int8, + OUT local_blks_dirtied int8, + OUT local_blks_written int8, + OUT temp_blks_read int8, + OUT temp_blks_written int8, + OUT shared_blk_read_time float8, + OUT shared_blk_write_time float8, + OUT local_blk_read_time float8, + OUT local_blk_write_time float8, + OUT temp_blk_read_time float8, + OUT temp_blk_write_time float8, + OUT wal_records int8, + OUT wal_fpi int8, + OUT wal_bytes numeric, + OUT jit_functions int8, + OUT jit_generation_time float8, + OUT jit_inlining_count int8, + OUT jit_inlining_time float8, + OUT jit_optimization_count int8, + OUT jit_optimization_time float8, + OUT jit_emission_count int8, + OUT jit_emission_time float8, + OUT jit_deform_count int8, + OUT jit_deform_time float8, + OUT parallel_workers_to_launch int8, + OUT parallel_workers_launched int8, + OUT stats_since timestamp with time zone, + OUT minmax_stats_since timestamp with time zone +) +RETURNS SETOF record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT VOLATILE PARALLEL SAFE; + +CREATE FUNCTION edb_stat_statements_info( + OUT dealloc bigint, + OUT stats_reset timestamp with time zone +) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT VOLATILE PARALLEL SAFE; + +-- Register views on the functions for ease of use. +CREATE VIEW edb_stat_statements AS + SELECT * FROM edb_stat_statements(true); + +GRANT SELECT ON edb_stat_statements TO PUBLIC; + +CREATE VIEW edb_stat_statements_info AS + SELECT * FROM edb_stat_statements_info(); + +GRANT SELECT ON edb_stat_statements_info TO PUBLIC; + +-- Don't want this to be available to non-superusers. +REVOKE ALL ON FUNCTION edb_stat_statements_reset(Oid, Oid[], bigint, boolean) FROM PUBLIC; diff --git a/edb_stat_statements/edb_stat_statements.c b/edb_stat_statements/edb_stat_statements.c new file mode 100644 index 00000000000..6b8b322d33d --- /dev/null +++ b/edb_stat_statements/edb_stat_statements.c @@ -0,0 +1,3297 @@ +/*------------------------------------------------------------------------- + * + * edb_stat_statements.c + * Track statement planning and execution times as well as resource + * usage across a whole database cluster. + * + * Execution costs are totaled for each distinct source query, and kept in + * a shared hashtable. (We track only as many distinct queries as will fit + * in the designated amount of shared memory.) + * + * Starting in Postgres 9.2, this module normalized query entries. As of + * Postgres 14, the normalization is done by the core if compute_query_id is + * enabled, or optionally by third-party modules. + * + * To facilitate presenting entries to users, we create "representative" query + * strings in which constants are replaced with parameter symbols ($n), to + * make it clearer what a normalized entry can represent. To save on shared + * memory, and to avoid having to truncate oversized query strings, we store + * these strings in a temporary external query-texts file. Offsets into this + * file are kept in shared memory. + * + * Note about locking issues: to create or delete an entry in the shared + * hashtable, one must hold pgss->lock exclusively. Modifying any field + * in an entry except the counters requires the same. To look up an entry, + * one must hold the lock shared. To read or update the counters within + * an entry, one must hold the lock shared or exclusive (so the entry doesn't + * disappear!) and also take the entry's mutex spinlock. + * The shared state variable pgss->extent (the next free spot in the external + * query-text file) should be accessed only while holding either the + * pgss->mutex spinlock, or exclusive lock on pgss->lock. We use the mutex to + * allow reserving file space while holding only shared lock on pgss->lock. + * Rewriting the entire external query-text file, eg for garbage collection, + * requires holding pgss->lock exclusively; this allows individual entries + * in the file to be read or written while holding only shared lock. + * + * + * Copyright (c) 2008-2024, PostgreSQL Global Development Group + * Copyright 2024-present MagicStack Inc. and the EdgeDB authors. + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include +#include +#include + +#include "access/parallel.h" +#include "catalog/pg_authid.h" +#include "common/hashfn.h" +#include "common/int.h" +#include "common/jsonapi.h" +#include "executor/instrument.h" +#include "funcapi.h" +#include "jit/jit.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "nodes/queryjumble.h" +#include "optimizer/planner.h" +#include "parser/analyze.h" +#include "parser/parsetree.h" +#include "parser/scanner.h" +#include "parser/scansup.h" +#include "pgstat.h" +#include "storage/fd.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "storage/spin.h" +#include "tcop/utility.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/jsonb.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" +#include "utils/uuid.h" + +PG_MODULE_MAGIC; + +#define EDB_STMT_MAGIC_PREFIX "-- {" + +/* Location of permanent stats file (valid when database is shut down) */ +#define PGSS_DUMP_FILE PGSTAT_STAT_PERMANENT_DIRECTORY "/edb_stat_statements.stat" + +/* + * Location of external query text file. + */ +#define PGSS_TEXT_FILE PG_STAT_TMP_DIR "/edbss_query_texts.stat" + +/* Magic number identifying the stats file format */ +static const uint32 PGSS_FILE_HEADER = 0x20241021; + +/* PostgreSQL major version number, changes in which invalidate all entries */ +static const uint32 PGSS_PG_MAJOR_VERSION = PG_VERSION_NUM / 100; + +/* XXX: Should USAGE_EXEC reflect execution time and/or buffer usage? */ +#define USAGE_EXEC(duration) (1.0) +#define USAGE_INIT (1.0) /* including initial planning */ +#define ASSUMED_MEDIAN_INIT (10.0) /* initial assumed median usage */ +#define ASSUMED_LENGTH_INIT 1024 /* initial assumed mean query length */ +#define USAGE_DECREASE_FACTOR (0.99) /* decreased every entry_dealloc */ +#define STICKY_DECREASE_FACTOR (0.50) /* factor for sticky entries */ +#define USAGE_DEALLOC_PERCENT 5 /* free this % of entries at once */ +#define IS_STICKY(c) ((c.calls[PGSS_PLAN] + c.calls[PGSS_EXEC]) == 0) + +/* + * Extension version number, for supporting older extension versions' objects + */ +typedef enum pgssVersion +{ + PGSS_V1_0 = 0, +} pgssVersion; + +typedef enum pgssStoreKind +{ + PGSS_INVALID = -1, + + /* + * PGSS_PLAN and PGSS_EXEC must be respectively 0 and 1 as they're used to + * reference the underlying values in the arrays in the Counters struct, + * and this order is required in edb_stat_statements_internal(). + */ + PGSS_PLAN = 0, + PGSS_EXEC, +} pgssStoreKind; + +#define PGSS_NUMKIND (PGSS_EXEC + 1) + +typedef enum EdbStmtType { + EDB_EDGEQL = 1, + EDB_SQL = 2, +} EdbStmtType; + +/* + * Internal states parsing the info JSON. + */ +typedef enum EdbStmtInfoParseState { + EDB_STMT_INFO_PARSE_NOOP = 0, + EDB_STMT_INFO_PARSE_QUERY = 1 << 0, + EDB_STMT_INFO_PARSE_ID = 1 << 1, + EDB_STMT_INFO_PARSE_TYPE = 1 << 2, + EDB_STMT_INFO_PARSE_EXTRAS = 1 << 3, +} EdbStmtInfoParseState; + +/* + * The info JSON parsing is only considered a success + * if all the fields listed below are found. + */ +#define EDB_STMT_INFO_PARSE_REQUIRED \ + (EDB_STMT_INFO_PARSE_QUERY \ + & EDB_STMT_INFO_PARSE_ID \ + & EDB_STMT_INFO_PARSE_TYPE \ + ) + +/* + * The result of parsing the info JSON by edbss_extract_stmt_info(). + */ +typedef struct EdbStmtInfo { + union { + pg_uuid_t uuid; + uint64 query_id; + } id; + const char *query; + int query_len; + EdbStmtType stmt_type; + Jsonb *extras; +} EdbStmtInfo; + +/* + * The custom "semantic state" structure for info JSON parsing. + * This is used internally as the `semstate` pointer of the parser, + * keeping track of: + * - level of nested JSON objects + * - known object keys we've found + * - current key/state we're parsing + * - pointer to the parse result struct + */ +typedef struct EdbStmtInfoSemState { + int nested_level; + uint found; + EdbStmtInfoParseState state; + EdbStmtInfo *info; +} EdbStmtInfoSemState; + +/* + * Hashtable key that defines the identity of a hashtable entry. We separate + * queries by user and by database even if they are otherwise identical. + * + * If you add a new key to this struct, make sure to teach pgss_store() to + * zero the padding bytes. Otherwise, things will break, because pgss_hash is + * created using HASH_BLOBS, and thus tag_hash is used to hash this. + + */ +typedef struct pgssHashKey +{ + Oid userid; /* user OID */ + Oid dbid; /* database OID */ + uint64 queryid; /* query identifier */ + bool toplevel; /* query executed at top level */ +} pgssHashKey; + +/* + * The actual stats counters kept within pgssEntry. + */ +typedef struct Counters +{ + int64 calls[PGSS_NUMKIND]; /* # of times planned/executed */ + double total_time[PGSS_NUMKIND]; /* total planning/execution time, + * in msec */ + double min_time[PGSS_NUMKIND]; /* minimum planning/execution time in + * msec since min/max reset */ + double max_time[PGSS_NUMKIND]; /* maximum planning/execution time in + * msec since min/max reset */ + double mean_time[PGSS_NUMKIND]; /* mean planning/execution time in + * msec */ + double sum_var_time[PGSS_NUMKIND]; /* sum of variances in + * planning/execution time in msec */ + int64 rows; /* total # of retrieved or affected rows */ + int64 shared_blks_hit; /* # of shared buffer hits */ + int64 shared_blks_read; /* # of shared disk blocks read */ + int64 shared_blks_dirtied; /* # of shared disk blocks dirtied */ + int64 shared_blks_written; /* # of shared disk blocks written */ + int64 local_blks_hit; /* # of local buffer hits */ + int64 local_blks_read; /* # of local disk blocks read */ + int64 local_blks_dirtied; /* # of local disk blocks dirtied */ + int64 local_blks_written; /* # of local disk blocks written */ + int64 temp_blks_read; /* # of temp blocks read */ + int64 temp_blks_written; /* # of temp blocks written */ + double shared_blk_read_time; /* time spent reading shared blocks, + * in msec */ + double shared_blk_write_time; /* time spent writing shared blocks, + * in msec */ + double local_blk_read_time; /* time spent reading local blocks, in + * msec */ + double local_blk_write_time; /* time spent writing local blocks, in + * msec */ + double temp_blk_read_time; /* time spent reading temp blocks, in msec */ + double temp_blk_write_time; /* time spent writing temp blocks, in + * msec */ + double usage; /* usage factor */ + int64 wal_records; /* # of WAL records generated */ + int64 wal_fpi; /* # of WAL full page images generated */ + uint64 wal_bytes; /* total amount of WAL generated in bytes */ + int64 jit_functions; /* total number of JIT functions emitted */ + double jit_generation_time; /* total time to generate jit code */ + int64 jit_inlining_count; /* number of times inlining time has been + * > 0 */ + double jit_deform_time; /* total time to deform tuples in jit code */ + int64 jit_deform_count; /* number of times deform time has been > + * 0 */ + + double jit_inlining_time; /* total time to inline jit code */ + int64 jit_optimization_count; /* number of times optimization time + * has been > 0 */ + double jit_optimization_time; /* total time to optimize jit code */ + int64 jit_emission_count; /* number of times emission time has been + * > 0 */ + double jit_emission_time; /* total time to emit jit code */ + int64 parallel_workers_to_launch; /* # of parallel workers planned + * to be launched */ + int64 parallel_workers_launched; /* # of parallel workers actually + * launched */ +} Counters; + +/* + * Global statistics for edb_stat_statements + */ +typedef struct pgssGlobalStats +{ + int64 dealloc; /* # of times entries were deallocated */ + TimestampTz stats_reset; /* timestamp with all stats reset */ +} pgssGlobalStats; + +/* + * Statistics per statement + * + * Note: in event of a failure in garbage collection of the query text file, + * we reset query_offset to zero and query_len to -1. This will be seen as + * an invalid state by qtext_fetch(). + */ +typedef struct pgssEntry +{ + pgssHashKey key; /* hash key of entry - MUST BE FIRST */ + Counters counters; /* the statistics for this query */ + Size query_offset; /* query text offset in external file */ + int query_len; /* # of valid bytes in query string, or -1 */ + int encoding; /* query text encoding */ + TimestampTz stats_since; /* timestamp of entry allocation */ + TimestampTz minmax_stats_since; /* timestamp of last min/max values reset */ + slock_t mutex; /* protects the counters only */ + + pg_uuid_t id; /* Full 16-bytes query ID as UUID */ + EdbStmtType stmt_type; /* Type of the EdgeDB query */ + int extras_len; /* # of valid bytes in extras jsonb, or -1 */ +} pgssEntry; + +/* + * Global shared state + */ +typedef struct pgssSharedState +{ + LWLock *lock; /* protects hashtable search/modification */ + double cur_median_usage; /* current median usage in hashtable */ + Size mean_query_len; /* current mean entry text length */ + slock_t mutex; /* protects following fields only: */ + Size extent; /* current extent of query file */ + int n_writers; /* number of active writers to query file */ + int gc_count; /* query file garbage collection cycle count */ + pgssGlobalStats stats; /* global statistics for pgss */ +} pgssSharedState; + +/*---- Local variables ----*/ + +static pg_uuid_t zero_uuid = { 0 }; + +/* Current nesting depth of planner/ExecutorRun/ProcessUtility calls */ +static int nesting_level = 0; + +/* Saved hook values in case of unload */ +static shmem_request_hook_type prev_shmem_request_hook = NULL; +static shmem_startup_hook_type prev_shmem_startup_hook = NULL; +static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL; +static planner_hook_type prev_planner_hook = NULL; +static ExecutorStart_hook_type prev_ExecutorStart = NULL; +static ExecutorRun_hook_type prev_ExecutorRun = NULL; +static ExecutorFinish_hook_type prev_ExecutorFinish = NULL; +static ExecutorEnd_hook_type prev_ExecutorEnd = NULL; +static ProcessUtility_hook_type prev_ProcessUtility = NULL; + +/* Links to shared memory state */ +static pgssSharedState *pgss = NULL; +static HTAB *pgss_hash = NULL; + +/*---- GUC variables ----*/ + +typedef enum +{ + PGSS_TRACK_NONE, /* track no statements */ + PGSS_TRACK_TOP, /* only top level statements */ + PGSS_TRACK_ALL, /* all statements, including nested ones */ +} PGSSTrackLevel; + +static const struct config_enum_entry track_options[] = +{ + {"none", PGSS_TRACK_NONE, false}, + {"top", PGSS_TRACK_TOP, false}, + {"all", PGSS_TRACK_ALL, false}, + {NULL, 0, false} +}; + +static int pgss_max = 5000; /* max # statements to track */ +static int pgss_track = PGSS_TRACK_TOP; /* tracking level */ +static bool pgss_track_utility = true; /* whether to track utility commands */ +static bool pgss_track_planning = false; /* whether to track planning + * duration */ +static bool pgss_save = true; /* whether to save stats across shutdown */ +static bool edbss_track_unrecognized = false; /* whether to track unrecognized statements as-is */ + + +#define pgss_enabled(level) \ + (!IsParallelWorker() && \ + (pgss_track == PGSS_TRACK_ALL || \ + (pgss_track == PGSS_TRACK_TOP && (level) == 0))) + +#define record_gc_qtexts() \ + do { \ + SpinLockAcquire(&pgss->mutex); \ + pgss->gc_count++; \ + SpinLockRelease(&pgss->mutex); \ + } while(0) + +/*---- Function declarations ----*/ + +PG_FUNCTION_INFO_V1(edb_stat_statements_reset); +PG_FUNCTION_INFO_V1(edb_stat_statements); +PG_FUNCTION_INFO_V1(edb_stat_statements_info); +PG_FUNCTION_INFO_V1(edb_stat_queryid); + +const char * +edbss_extract_info_line(const char *s, int* len); +EdbStmtInfo * +edbss_extract_stmt_info(const char* query_str, int query_len); +static inline void +edbss_free_stmt_info(EdbStmtInfo *info); +static JsonParseErrorType +edbss_json_struct_start(void *semstate); +static JsonParseErrorType +edbss_json_struct_end(void *semstate); +static JsonParseErrorType +edbss_json_ofield_start(void *semstate, char *fname, bool isnull); +static JsonParseErrorType +edbss_json_scalar(void *semstate, char *token, JsonTokenType tokenType); + +static void pgss_shmem_request(void); +static void pgss_shmem_startup(void); +static void pgss_shmem_shutdown(int code, Datum arg); +static void pgss_post_parse_analyze(ParseState *pstate, Query *query, + JumbleState *jstate); +static PlannedStmt *pgss_planner(Query *parse, + const char *query_string, + int cursorOptions, + ParamListInfo boundParams); +static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags); +static void pgss_ExecutorRun(QueryDesc *queryDesc, + ScanDirection direction, + uint64 count, bool execute_once); +static void pgss_ExecutorFinish(QueryDesc *queryDesc); +static void pgss_ExecutorEnd(QueryDesc *queryDesc); +static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, + bool readOnlyTree, + ProcessUtilityContext context, ParamListInfo params, + QueryEnvironment *queryEnv, + DestReceiver *dest, QueryCompletion *qc); +static void pgss_store(const char *query, uint64 queryId, + int query_location, int query_len, + pgssStoreKind kind, + double total_time, uint64 rows, + const BufferUsage *bufusage, + const WalUsage *walusage, + const struct JitInstrumentation *jitusage, + JumbleState *jstate, + bool edb_extracted, + pg_uuid_t *id, + EdbStmtType stmt_type, + const Jsonb *extras, + int parallel_workers_to_launch, + int parallel_workers_launched); +static void edb_stat_statements_internal(FunctionCallInfo fcinfo, + pgssVersion api_version, + bool showtext); +static Size pgss_memsize(void); +static pgssEntry *entry_alloc(pgssHashKey *key, Size query_offset, int query_len, + int encoding, bool sticky, pg_uuid_t *id, + EdbStmtType stmt_type, int extras_len); +static void entry_dealloc(void); +static bool qtext_store(const char *query, int query_len, + const Jsonb *extras, int extras_len, + Size *query_offset, int *gc_count); +static char *qtext_load_file(Size *buffer_size); +static char *qtext_fetch(Size query_offset, int query_len, + char *buffer, Size buffer_size); +static bool need_gc_qtexts(void); +static void gc_qtexts(void); +static TimestampTz entry_reset(Oid userid, const Datum *dbids, int dbids_len, uint64 queryid, bool minmax_only); +static char *generate_normalized_query(JumbleState *jstate, const char *query, + int query_loc, int *query_len_p); +static void fill_in_constant_lengths(JumbleState *jstate, const char *query, + int query_loc); +static int comp_location(const void *a, const void *b); + + +/* + * Module load callback + */ +void +_PG_init(void) +{ + /* + * In order to create our shared memory area, we have to be loaded via + * shared_preload_libraries. If not, fall out without hooking into any of + * the main system. (We don't throw error here because it seems useful to + * allow the edb_stat_statements functions to be created even when the + * module isn't active. The functions must protect themselves against + * being called then, however.) + */ + if (!process_shared_preload_libraries_in_progress) + return; + + /* + * Inform the postmaster that we want to enable query_id calculation if + * compute_query_id is set to auto. + */ + EnableQueryId(); + + /* + * Define (or redefine) custom GUC variables. + */ + DefineCustomIntVariable("edb_stat_statements.max", + "Sets the maximum number of statements tracked by edb_stat_statements.", + NULL, + &pgss_max, + 5000, + 100, + INT_MAX / 2, + PGC_POSTMASTER, + 0, + NULL, + NULL, + NULL); + + DefineCustomEnumVariable("edb_stat_statements.track", + "Selects which statements are tracked by edb_stat_statements.", + NULL, + &pgss_track, + PGSS_TRACK_TOP, + track_options, + PGC_SUSET, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("edb_stat_statements.track_utility", + "Selects whether utility commands are tracked by edb_stat_statements.", + NULL, + &pgss_track_utility, + true, + PGC_SUSET, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("edb_stat_statements.track_planning", + "Selects whether planning duration is tracked by edb_stat_statements.", + NULL, + &pgss_track_planning, + false, + PGC_SUSET, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("edb_stat_statements.save", + "Save edb_stat_statements statistics across server shutdowns.", + NULL, + &pgss_save, + true, + PGC_SIGHUP, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("edb_stat_statements.track_unrecognized", + "Selects whether unrecognized SQL statements are tracked as-is.", + NULL, + &edbss_track_unrecognized, + false, + PGC_SIGHUP, + 0, + NULL, + NULL, + NULL); + + MarkGUCPrefixReserved("edb_stat_statements"); + + /* + * Install hooks. + */ + prev_shmem_request_hook = shmem_request_hook; + shmem_request_hook = pgss_shmem_request; + prev_shmem_startup_hook = shmem_startup_hook; + shmem_startup_hook = pgss_shmem_startup; + prev_post_parse_analyze_hook = post_parse_analyze_hook; + post_parse_analyze_hook = pgss_post_parse_analyze; + prev_planner_hook = planner_hook; + planner_hook = pgss_planner; + prev_ExecutorStart = ExecutorStart_hook; + ExecutorStart_hook = pgss_ExecutorStart; + prev_ExecutorRun = ExecutorRun_hook; + ExecutorRun_hook = pgss_ExecutorRun; + prev_ExecutorFinish = ExecutorFinish_hook; + ExecutorFinish_hook = pgss_ExecutorFinish; + prev_ExecutorEnd = ExecutorEnd_hook; + ExecutorEnd_hook = pgss_ExecutorEnd; + prev_ProcessUtility = ProcessUtility_hook; + ProcessUtility_hook = pgss_ProcessUtility; +} + +const char * +edbss_extract_info_line(const char *s, int *len) { + int prefix_len = strlen(EDB_STMT_MAGIC_PREFIX); + if (*len > prefix_len && strncmp(s, EDB_STMT_MAGIC_PREFIX, prefix_len) == 0) { + const char *rv = s + 3; // skip "-- " + int remaining_len = *len - prefix_len; + int rv_len = 0; + while (rv_len < remaining_len && rv[rv_len] != '\n') + rv_len++; + if (rv_len > 0) { + *len = rv_len; + return rv; + } + } + return NULL; +} + +/* + * Extract EdgeDB query info from the JSON in the leading comments. + * If success, returns a palloc-ed EdbStmtInfo which must be freed + * after usage with edbss_free_stmt_info(). + * + * The query info JSON comments must be at the beginning of the + * query_str. Each line must start with `-- {` and end with `\n`, + * with a single valid JSON string. The JSON string itself must + * not contain any `\n`, or it'll be treated as a bad JSON. + * + * This function scans over all such lines and records known + * values progressively. Malformed JSONs may be partially read, + * this function won't bail just because of that; it'll continue + * with the next line. If the same key exists more than once, + * only the first occurrence is effective, later ones are ignored. + * This function returns successfully as soon as all required + * fields (EDB_STMT_INFO_PARSE_REQUIRED) are found AND the current + * JSON is in good form, ignoring remaining lines. For example: + * + * -- {"a": 1} + * -- {"a": 11, "d": 4, "nested": {"b": 22}} + * -- {"b": 2, "unknown": "skipped", + * -- {"c": 3} + * -- {"e": 5} + * SELECT .... + * + * If the required fields are {a, b, c}, while {d, e} are known + * but not required, the extracted info will be: + * + * {"a": 1, "b": 2, "c": 3, "d": 4} + * + */ +EdbStmtInfo * +edbss_extract_stmt_info(const char* query_str, int query_len) { + int info_len = query_len; + const char *info_str = edbss_extract_info_line(query_str, &info_len); + + if (info_str) { + EdbStmtInfo *info = (EdbStmtInfo *) palloc0(sizeof(EdbStmtInfo)); + EdbStmtInfoSemState state = { + .info = info, + .state = EDB_STMT_INFO_PARSE_NOOP, + }; + JsonSemAction sem = { + .semstate = (void *) &state, + .object_start = edbss_json_struct_start, + .object_end = edbss_json_struct_end, + .array_start = edbss_json_struct_start, + .array_end = edbss_json_struct_end, + .object_field_start = edbss_json_ofield_start, + .scalar = edbss_json_scalar, + }; + + while (info_str) { + JsonLexContext *lex = makeJsonLexContextCstringLen( +#if PG_VERSION_NUM >= 170000 + NULL, + info_str, +#else + (char *) info_str, // not actually mutating +#endif + info_len, + PG_UTF8, + true); + JsonParseErrorType parse_rv = pg_parse_json(lex, &sem); + freeJsonLexContext(lex); + + if (parse_rv == JSON_SUCCESS) + if ((state.found & EDB_STMT_INFO_PARSE_REQUIRED) == EDB_STMT_INFO_PARSE_REQUIRED) + return info->id.query_id != UINT64CONST(0) ? info : NULL; + + info_str += info_len + 1; + info_len = query_len - (int)(info_str - query_str); + info_str = edbss_extract_info_line(info_str, &info_len); + state.nested_level = 0; + state.state = EDB_STMT_INFO_PARSE_NOOP; + } + edbss_free_stmt_info(info); + } + + return NULL; +} + +/* + * Frees the given EdbStmtInfo struct as well as + * its owning sub-fields (query). + */ +static inline void +edbss_free_stmt_info(EdbStmtInfo *info) { + Assert(info != NULL); + pfree((void *) info->query); + pfree(info); +} + +static JsonParseErrorType +edbss_json_struct_start(void *semstate) { + EdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate; + state->nested_level += 1; + return JSON_SUCCESS; +} + +static JsonParseErrorType +edbss_json_struct_end(void *semstate) { + EdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate; + state->nested_level -= 1; + return JSON_SUCCESS; +} + +static JsonParseErrorType +edbss_json_ofield_start(void *semstate, char *fname, bool isnull) { + EdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate; + Assert(fname != NULL); + if (state->nested_level == 1) { + if (strcmp(fname, "query") == 0) { + state->state = EDB_STMT_INFO_PARSE_QUERY; + } else if (strcmp(fname, "id") == 0) { + state->state = EDB_STMT_INFO_PARSE_ID; + } else if (strcmp(fname, "type") == 0) { + state->state = EDB_STMT_INFO_PARSE_TYPE; + } else if (strcmp(fname, "extras") == 0) { + state->state = EDB_STMT_INFO_PARSE_EXTRAS; + } + } + pfree(fname); /* must not use object_field_end */ + return JSON_SUCCESS; +} + +static JsonParseErrorType +edbss_json_scalar(void *semstate, char *token, JsonTokenType tokenType) { + EdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate; + Assert(token != NULL); + + if (state->found & state->state) { + pfree(token); + state->state = EDB_STMT_INFO_PARSE_NOOP; + return JSON_SUCCESS; + } + + switch (state->state) { + case EDB_STMT_INFO_PARSE_QUERY: + if (tokenType == JSON_TOKEN_STRING) { + state->info->query = token; + state->info->query_len = (int) strlen(token); + break; + } + goto fail; + + case EDB_STMT_INFO_PARSE_ID: + if (tokenType == JSON_TOKEN_STRING) { + Datum id_datum = DirectFunctionCall1(uuid_in, CStringGetDatum(token)); + pg_uuid_t *id_ptr = DatumGetUUIDP(id_datum); + state->info->id.uuid = *id_ptr; + pfree(id_ptr); + pfree(token); + break; + } + goto fail; + + case EDB_STMT_INFO_PARSE_TYPE: + if (tokenType == JSON_TOKEN_NUMBER) { + char *endptr; + long type_val = strtol(token, &endptr, 10); + if (*endptr == '\0' && type_val != LONG_MAX) { + if (type_val == EDB_EDGEQL || type_val == EDB_SQL) { + state->info->stmt_type = type_val; + pfree(token); + break; + } + } + } + goto fail; + + case EDB_STMT_INFO_PARSE_EXTRAS: + if (tokenType == JSON_TOKEN_STRING) { + Datum extras_jsonb = DirectFunctionCall1(jsonb_in, CStringGetDatum(token)); + state->info->extras = DatumGetJsonbP(extras_jsonb); + pfree(token); + break; + } + goto fail; + + case EDB_STMT_INFO_PARSE_NOOP: + pfree(token); + return JSON_SUCCESS; + } + state->found |= state->state; + state->state = EDB_STMT_INFO_PARSE_NOOP; + return JSON_SUCCESS; + +fail: + pfree(token); + return JSON_SEM_ACTION_FAILED; +} + +/* + * shmem_request hook: request additional shared resources. We'll allocate or + * attach to the shared resources in pgss_shmem_startup(). + */ +static void +pgss_shmem_request(void) +{ + if (prev_shmem_request_hook) + prev_shmem_request_hook(); + + RequestAddinShmemSpace(pgss_memsize()); + RequestNamedLWLockTranche("edb_stat_statements", 1); +} + +/* + * shmem_startup hook: allocate or attach to shared memory, + * then load any pre-existing statistics from file. + * Also create and load the query-texts file, which is expected to exist + * (even if empty) while the module is enabled. + */ +static void +pgss_shmem_startup(void) +{ + bool found; + HASHCTL info; + FILE *file = NULL; + FILE *qfile = NULL; + uint32 header; + int32 num; + int32 pgver; + int32 i; + int buffer_size; + char *buffer = NULL; + + if (prev_shmem_startup_hook) + prev_shmem_startup_hook(); + + /* reset in case this is a restart within the postmaster */ + pgss = NULL; + pgss_hash = NULL; + + /* + * Create or attach to the shared memory state, including hash table + */ + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + + pgss = ShmemInitStruct("edb_stat_statements", + sizeof(pgssSharedState), + &found); + + if (!found) + { + /* First time through ... */ + pgss->lock = &(GetNamedLWLockTranche("edb_stat_statements"))->lock; + pgss->cur_median_usage = ASSUMED_MEDIAN_INIT; + pgss->mean_query_len = ASSUMED_LENGTH_INIT; + SpinLockInit(&pgss->mutex); + pgss->extent = 0; + pgss->n_writers = 0; + pgss->gc_count = 0; + pgss->stats.dealloc = 0; + pgss->stats.stats_reset = GetCurrentTimestamp(); + } + + info.keysize = sizeof(pgssHashKey); + info.entrysize = sizeof(pgssEntry); + pgss_hash = ShmemInitHash("edb_stat_statements hash", + pgss_max, pgss_max, + &info, + HASH_ELEM | HASH_BLOBS); + + LWLockRelease(AddinShmemInitLock); + + /* + * If we're in the postmaster (or a standalone backend...), set up a shmem + * exit hook to dump the statistics to disk. + */ + if (!IsUnderPostmaster) + on_shmem_exit(pgss_shmem_shutdown, (Datum) 0); + + /* + * Done if some other process already completed our initialization. + */ + if (found) + return; + + /* + * Note: we don't bother with locks here, because there should be no other + * processes running when this code is reached. + */ + + /* Unlink query text file possibly left over from crash */ + unlink(PGSS_TEXT_FILE); + + /* Allocate new query text temp file */ + qfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W); + if (qfile == NULL) + goto write_error; + + /* + * If we were told not to load old statistics, we're done. (Note we do + * not try to unlink any old dump file in this case. This seems a bit + * questionable but it's the historical behavior.) + */ + if (!pgss_save) + { + FreeFile(qfile); + return; + } + + /* + * Attempt to load old statistics from the dump file. + */ + file = AllocateFile(PGSS_DUMP_FILE, PG_BINARY_R); + if (file == NULL) + { + if (errno != ENOENT) + goto read_error; + /* No existing persisted stats file, so we're done */ + FreeFile(qfile); + return; + } + + buffer_size = 2048; + buffer = (char *) palloc(buffer_size); + + if (fread(&header, sizeof(uint32), 1, file) != 1 || + fread(&pgver, sizeof(uint32), 1, file) != 1 || + fread(&num, sizeof(int32), 1, file) != 1) + goto read_error; + + if (header != PGSS_FILE_HEADER || + pgver != PGSS_PG_MAJOR_VERSION) + goto data_error; + + for (i = 0; i < num; i++) + { + pgssEntry temp; + pgssEntry *entry; + Size query_offset; + int len; + + if (fread(&temp, sizeof(pgssEntry), 1, file) != 1) + goto read_error; + + /* Encoding is the only field we can easily sanity-check */ + if (!PG_VALID_BE_ENCODING(temp.encoding)) + goto data_error; + + len = temp.query_len + temp.extras_len; + + /* Resize buffer as needed */ + if (len >= buffer_size) + { + buffer_size = Max(buffer_size * 2, len + 1); + buffer = repalloc(buffer, buffer_size); + } + + if (fread(buffer, 1, len + 1, file) != len + 1) + goto read_error; + + /* Should have a trailing null, but let's make sure */ + buffer[len] = '\0'; + + /* Skip loading "sticky" entries */ + if (IS_STICKY(temp.counters)) + continue; + + /* Store the query text */ + query_offset = pgss->extent; + if (fwrite(buffer, 1, len + 1, qfile) != len + 1) + goto write_error; + pgss->extent += len + 1; + + /* make the hashtable entry (discards old entries if too many) */ + entry = entry_alloc(&temp.key, query_offset, temp.query_len, + temp.encoding, + false, NULL, 0, temp.extras_len); + + /* copy in the actual stats */ + entry->counters = temp.counters; + entry->stats_since = temp.stats_since; + entry->minmax_stats_since = temp.minmax_stats_since; + entry->id = temp.id; + entry->stmt_type = temp.stmt_type; + } + + /* Read global statistics for edb_stat_statements */ + if (fread(&pgss->stats, sizeof(pgssGlobalStats), 1, file) != 1) + goto read_error; + + pfree(buffer); + FreeFile(file); + FreeFile(qfile); + + /* + * Remove the persisted stats file so it's not included in + * backups/replication standbys, etc. A new file will be written on next + * shutdown. + * + * Note: it's okay if the PGSS_TEXT_FILE is included in a basebackup, + * because we remove that file on startup; it acts inversely to + * PGSS_DUMP_FILE, in that it is only supposed to be around when the + * server is running, whereas PGSS_DUMP_FILE is only supposed to be around + * when the server is not running. Leaving the file creates no danger of + * a newly restored database having a spurious record of execution costs, + * which is what we're really concerned about here. + */ + unlink(PGSS_DUMP_FILE); + + return; + +read_error: + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", + PGSS_DUMP_FILE))); + goto fail; +data_error: + ereport(LOG, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("ignoring invalid data in file \"%s\"", + PGSS_DUMP_FILE))); + goto fail; +write_error: + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + PGSS_TEXT_FILE))); +fail: + if (buffer) + pfree(buffer); + if (file) + FreeFile(file); + if (qfile) + FreeFile(qfile); + /* If possible, throw away the bogus file; ignore any error */ + unlink(PGSS_DUMP_FILE); + + /* + * Don't unlink PGSS_TEXT_FILE here; it should always be around while the + * server is running with edb_stat_statements enabled + */ +} + +/* + * shmem_shutdown hook: Dump statistics into file. + * + * Note: we don't bother with acquiring lock, because there should be no + * other processes running when this is called. + */ +static void +pgss_shmem_shutdown(int code, Datum arg) +{ + FILE *file; + char *qbuffer = NULL; + Size qbuffer_size = 0; + HASH_SEQ_STATUS hash_seq; + int32 num_entries; + pgssEntry *entry; + + /* Don't try to dump during a crash. */ + if (code) + return; + + /* Safety check ... shouldn't get here unless shmem is set up. */ + if (!pgss || !pgss_hash) + return; + + /* Don't dump if told not to. */ + if (!pgss_save) + return; + + file = AllocateFile(PGSS_DUMP_FILE ".tmp", PG_BINARY_W); + if (file == NULL) + goto error; + + if (fwrite(&PGSS_FILE_HEADER, sizeof(uint32), 1, file) != 1) + goto error; + if (fwrite(&PGSS_PG_MAJOR_VERSION, sizeof(uint32), 1, file) != 1) + goto error; + num_entries = hash_get_num_entries(pgss_hash); + if (fwrite(&num_entries, sizeof(int32), 1, file) != 1) + goto error; + + qbuffer = qtext_load_file(&qbuffer_size); + if (qbuffer == NULL) + goto error; + + /* + * When serializing to disk, we store query texts immediately after their + * entry data. Any orphaned query texts are thereby excluded. + */ + hash_seq_init(&hash_seq, pgss_hash); + while ((entry = hash_seq_search(&hash_seq)) != NULL) + { + int len = entry->query_len + entry->extras_len; + char *qstr = qtext_fetch(entry->query_offset, len, + qbuffer, qbuffer_size); + + if (qstr == NULL) + continue; /* Ignore any entries with bogus texts */ + + if (fwrite(entry, sizeof(pgssEntry), 1, file) != 1 || + fwrite(qstr, 1, len + 1, file) != len + 1) + { + /* note: we assume hash_seq_term won't change errno */ + hash_seq_term(&hash_seq); + goto error; + } + } + + /* Dump global statistics for edb_stat_statements */ + if (fwrite(&pgss->stats, sizeof(pgssGlobalStats), 1, file) != 1) + goto error; + + free(qbuffer); + qbuffer = NULL; + + if (FreeFile(file)) + { + file = NULL; + goto error; + } + + /* + * Rename file into place, so we atomically replace any old one. + */ + (void) durable_rename(PGSS_DUMP_FILE ".tmp", PGSS_DUMP_FILE, LOG); + + /* Unlink query-texts file; it's not needed while shutdown */ + unlink(PGSS_TEXT_FILE); + + return; + +error: + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + PGSS_DUMP_FILE ".tmp"))); + free(qbuffer); + if (file) + FreeFile(file); + unlink(PGSS_DUMP_FILE ".tmp"); + unlink(PGSS_TEXT_FILE); +} + +/* + * Post-parse-analysis hook: mark query with a queryId + */ +static void +pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) +{ + EdbStmtInfo *info; + const char *query_str; + int query_location, query_len; + + if (prev_post_parse_analyze_hook) + prev_post_parse_analyze_hook(pstate, query, jstate); + + /* Safety check... */ + if (!pgss || !pgss_hash || !pgss_enabled(nesting_level)) + return; + + /* + * If it's EXECUTE, clear the queryId so that stats will accumulate for + * the underlying PREPARE. But don't do this if we're not tracking + * utility statements, to avoid messing up another extension that might be + * tracking them. + */ + if (query->utilityStmt) + { + if (pgss_track_utility && IsA(query->utilityStmt, ExecuteStmt)) + { + query->queryId = UINT64CONST(0); + return; + } + } + + /* Parse EdgeDB query info JSON and overwrite query->queryId */ + query_location = query->stmt_location; + query_len = query->stmt_len; + query_str = CleanQuerytext(pstate->p_sourcetext, &query_location, &query_len); + if ((info = edbss_extract_stmt_info(query_str, query_len)) != NULL) { + query->queryId = info->id.query_id; + + /* We immediately create a hash table entry for the query, + * so that we don't need to parse the query info JSON later + * again for the query with the same queryId. + */ + pgss_store(info->query, + info->id.query_id, + 0, + info->query_len, + PGSS_INVALID, + 0, + 0, + NULL, + NULL, + NULL, + NULL, + true, + &info->id.uuid, + info->stmt_type, + info->extras, + 0, + 0); + edbss_free_stmt_info(info); + } else if (!edbss_track_unrecognized) { + query->queryId = UINT64CONST(0); + } else if (jstate && jstate->clocations_count > 0) + /* + * If query jumbling were able to identify any ignorable constants, we + * immediately create a hash table entry for the query, so that we can + * record the normalized form of the query string. If there were no such + * constants, the normalized string would be the same as the query text + * anyway, so there's no need for an early entry. + */ + pgss_store(pstate->p_sourcetext, + query->queryId, + query->stmt_location, + query->stmt_len, + PGSS_INVALID, + 0, + 0, + NULL, + NULL, + NULL, + jstate, + true, + NULL, + 0, + NULL, + 0, + 0); +} + +/* + * Planner hook: forward to regular planner, but measure planning time + * if needed. + */ +static PlannedStmt * +pgss_planner(Query *parse, + const char *query_string, + int cursorOptions, + ParamListInfo boundParams) +{ + PlannedStmt *result; + + /* + * We can't process the query if no query_string is provided, as + * pgss_store needs it. We also ignore query without queryid, as it would + * be treated as a utility statement, which may not be the case. + */ + if (pgss_enabled(nesting_level) + && pgss_track_planning && query_string + && parse->queryId != UINT64CONST(0)) + { + instr_time start; + instr_time duration; + BufferUsage bufusage_start, + bufusage; + WalUsage walusage_start, + walusage; + + /* We need to track buffer usage as the planner can access them. */ + bufusage_start = pgBufferUsage; + + /* + * Similarly the planner could write some WAL records in some cases + * (e.g. setting a hint bit with those being WAL-logged) + */ + walusage_start = pgWalUsage; + INSTR_TIME_SET_CURRENT(start); + + nesting_level++; + PG_TRY(); + { + if (prev_planner_hook) + result = prev_planner_hook(parse, query_string, cursorOptions, + boundParams); + else + result = standard_planner(parse, query_string, cursorOptions, + boundParams); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); + + INSTR_TIME_SET_CURRENT(duration); + INSTR_TIME_SUBTRACT(duration, start); + + /* calc differences of buffer counters. */ + memset(&bufusage, 0, sizeof(BufferUsage)); + BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start); + + /* calc differences of WAL counters. */ + memset(&walusage, 0, sizeof(WalUsage)); + WalUsageAccumDiff(&walusage, &pgWalUsage, &walusage_start); + + pgss_store(query_string, + parse->queryId, + parse->stmt_location, + parse->stmt_len, + PGSS_PLAN, + INSTR_TIME_GET_MILLISEC(duration), + 0, + &bufusage, + &walusage, + NULL, + NULL, + false, + NULL, + 0, + NULL, + 0, + 0); + } + else + { + /* + * Even though we're not tracking plan time for this statement, we + * must still increment the nesting level, to ensure that functions + * evaluated during planning are not seen as top-level calls. + */ + nesting_level++; + PG_TRY(); + { + if (prev_planner_hook) + result = prev_planner_hook(parse, query_string, cursorOptions, + boundParams); + else + result = standard_planner(parse, query_string, cursorOptions, + boundParams); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); + } + + return result; +} + +/* + * ExecutorStart hook: start up tracking if needed + */ +static void +pgss_ExecutorStart(QueryDesc *queryDesc, int eflags) +{ + if (prev_ExecutorStart) + prev_ExecutorStart(queryDesc, eflags); + else + standard_ExecutorStart(queryDesc, eflags); + + /* + * If query has queryId zero, don't track it. This prevents double + * counting of optimizable statements that are directly contained in + * utility statements. + */ + if (pgss_enabled(nesting_level) && queryDesc->plannedstmt->queryId != UINT64CONST(0)) + { + /* + * Set up to track total elapsed time in ExecutorRun. Make sure the + * space is allocated in the per-query context so it will go away at + * ExecutorEnd. + */ + if (queryDesc->totaltime == NULL) + { + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt); + queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false); + MemoryContextSwitchTo(oldcxt); + } + } +} + +/* + * ExecutorRun hook: all we need do is track nesting depth + */ +static void +pgss_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count, + bool execute_once) +{ + nesting_level++; + PG_TRY(); + { + if (prev_ExecutorRun) + prev_ExecutorRun(queryDesc, direction, count, execute_once); + else + standard_ExecutorRun(queryDesc, direction, count, execute_once); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); +} + +/* + * ExecutorFinish hook: all we need do is track nesting depth + */ +static void +pgss_ExecutorFinish(QueryDesc *queryDesc) +{ + nesting_level++; + PG_TRY(); + { + if (prev_ExecutorFinish) + prev_ExecutorFinish(queryDesc); + else + standard_ExecutorFinish(queryDesc); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); +} + +/* + * ExecutorEnd hook: store results if needed + */ +static void +pgss_ExecutorEnd(QueryDesc *queryDesc) +{ + uint64 queryId = queryDesc->plannedstmt->queryId; + + if (queryId != UINT64CONST(0) && queryDesc->totaltime && + pgss_enabled(nesting_level)) + { + /* + * Make sure stats accumulation is done. (Note: it's okay if several + * levels of hook all do this.) + */ + InstrEndLoop(queryDesc->totaltime); + + pgss_store(queryDesc->sourceText, + queryId, + queryDesc->plannedstmt->stmt_location, + queryDesc->plannedstmt->stmt_len, + PGSS_EXEC, + queryDesc->totaltime->total * 1000.0, /* convert to msec */ + queryDesc->estate->es_total_processed, + &queryDesc->totaltime->bufusage, + &queryDesc->totaltime->walusage, + queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL, + NULL, + false, + NULL, + 0, + NULL, +#if PG_VERSION_NUM >= 180000 + queryDesc->estate->es_parallel_workers_to_launch, + queryDesc->estate->es_parallel_workers_launched +#else + 0, + 0 +#endif + ); + } + + if (prev_ExecutorEnd) + prev_ExecutorEnd(queryDesc); + else + standard_ExecutorEnd(queryDesc); +} + +/* + * ProcessUtility hook + */ +static void +pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, + bool readOnlyTree, + ProcessUtilityContext context, + ParamListInfo params, QueryEnvironment *queryEnv, + DestReceiver *dest, QueryCompletion *qc) +{ + Node *parsetree = pstmt->utilityStmt; + uint64 saved_queryId = pstmt->queryId; + int saved_stmt_location = pstmt->stmt_location; + int saved_stmt_len = pstmt->stmt_len; + bool enabled = pgss_track_utility && pgss_enabled(nesting_level); + + /* + * Force utility statements to get queryId zero. We do this even in cases + * where the statement contains an optimizable statement for which a + * queryId could be derived (such as EXPLAIN or DECLARE CURSOR). For such + * cases, runtime control will first go through ProcessUtility and then + * the executor, and we don't want the executor hooks to do anything, + * since we are already measuring the statement's costs at the utility + * level. + * + * Note that this is only done if edb_stat_statements is enabled and + * configured to track utility statements, in the unlikely possibility + * that user configured another extension to handle utility statements + * only. + */ + if (enabled) + pstmt->queryId = UINT64CONST(0); + + /* + * If it's an EXECUTE statement, we don't track it and don't increment the + * nesting level. This allows the cycles to be charged to the underlying + * PREPARE instead (by the Executor hooks), which is much more useful. + * + * We also don't track execution of PREPARE. If we did, we would get one + * hash table entry for the PREPARE (with hash calculated from the query + * string), and then a different one with the same query string (but hash + * calculated from the query tree) would be used to accumulate costs of + * ensuing EXECUTEs. This would be confusing. Since PREPARE doesn't + * actually run the planner (only parse+rewrite), its costs are generally + * pretty negligible and it seems okay to just ignore it. + */ + if (enabled && + !IsA(parsetree, ExecuteStmt) && + !IsA(parsetree, PrepareStmt)) + { + instr_time start; + instr_time duration; + uint64 rows; + BufferUsage bufusage_start, + bufusage; + WalUsage walusage_start, + walusage; + + bufusage_start = pgBufferUsage; + walusage_start = pgWalUsage; + INSTR_TIME_SET_CURRENT(start); + + nesting_level++; + PG_TRY(); + { + if (prev_ProcessUtility) + prev_ProcessUtility(pstmt, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + else + standard_ProcessUtility(pstmt, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); + + /* + * CAUTION: do not access the *pstmt data structure again below here. + * If it was a ROLLBACK or similar, that data structure may have been + * freed. We must copy everything we still need into local variables, + * which we did above. + * + * For the same reason, we can't risk restoring pstmt->queryId to its + * former value, which'd otherwise be a good idea. + */ + + INSTR_TIME_SET_CURRENT(duration); + INSTR_TIME_SUBTRACT(duration, start); + + /* + * Track the total number of rows retrieved or affected by the utility + * statements of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED + * VIEW, REFRESH MATERIALIZED VIEW and SELECT INTO. + */ + rows = (qc && (qc->commandTag == CMDTAG_COPY || + qc->commandTag == CMDTAG_FETCH || + qc->commandTag == CMDTAG_SELECT || + qc->commandTag == CMDTAG_REFRESH_MATERIALIZED_VIEW)) ? + qc->nprocessed : 0; + + /* calc differences of buffer counters. */ + memset(&bufusage, 0, sizeof(BufferUsage)); + BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start); + + /* calc differences of WAL counters. */ + memset(&walusage, 0, sizeof(WalUsage)); + WalUsageAccumDiff(&walusage, &pgWalUsage, &walusage_start); + + pgss_store(queryString, + saved_queryId, + saved_stmt_location, + saved_stmt_len, + PGSS_EXEC, + INSTR_TIME_GET_MILLISEC(duration), + rows, + &bufusage, + &walusage, + NULL, + NULL, + false, + NULL, + 0, + NULL, + 0, + 0); + } + else + { + /* + * Even though we're not tracking execution time for this statement, + * we must still increment the nesting level, to ensure that functions + * evaluated within it are not seen as top-level calls. But don't do + * so for EXECUTE; that way, when control reaches pgss_planner or + * pgss_ExecutorStart, we will treat the costs as top-level if + * appropriate. Likewise, don't bump for PREPARE, so that parse + * analysis will treat the statement as top-level if appropriate. + * + * To be absolutely certain we don't mess up the nesting level, + * evaluate the bump_level condition just once. + */ + bool bump_level = + !IsA(parsetree, ExecuteStmt) && + !IsA(parsetree, PrepareStmt); + + if (bump_level) + nesting_level++; + PG_TRY(); + { + if (prev_ProcessUtility) + prev_ProcessUtility(pstmt, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + else + standard_ProcessUtility(pstmt, queryString, readOnlyTree, + context, params, queryEnv, + dest, qc); + } + PG_FINALLY(); + { + if (bump_level) + nesting_level--; + } + PG_END_TRY(); + } +} + +/* + * Store some statistics for a statement. + * + * If jstate is not NULL then we're trying to create an entry for which + * we have no statistics as yet; we just want to record the normalized + * query string. total_time, rows, bufusage and walusage are ignored in this + * case. + * + * If kind is PGSS_PLAN or PGSS_EXEC, its value is used as the array position + * for the arrays in the Counters field. + */ +static void +pgss_store(const char *query, uint64 queryId, + int query_location, int query_len, + pgssStoreKind kind, + double total_time, uint64 rows, + const BufferUsage *bufusage, + const WalUsage *walusage, + const struct JitInstrumentation *jitusage, + JumbleState *jstate, + bool edb_extracted, + pg_uuid_t *id, + EdbStmtType stmt_type, + const Jsonb *extras, + int parallel_workers_to_launch, + int parallel_workers_launched) +{ + pgssHashKey key; + pgssEntry *entry; + char *norm_query = NULL; + int encoding = GetDatabaseEncoding(); + EdbStmtInfo *info = NULL; + + Assert(query != NULL); + + /* Safety check... */ + if (!pgss || !pgss_hash) + return; + + /* + * Nothing to do if compute_query_id isn't enabled and no other module + * computed a query identifier. + */ + if (queryId == UINT64CONST(0)) + return; + + /* + * Confine our attention to the relevant part of the string, if the query + * is a portion of a multi-statement source string, and update query + * location and length if needed. + */ + query = CleanQuerytext(query, &query_location, &query_len); + + /* Set up key for hashtable search */ + + /* clear padding */ + memset(&key, 0, sizeof(pgssHashKey)); + + key.userid = GetUserId(); + key.dbid = MyDatabaseId; + key.queryid = queryId; + key.toplevel = (nesting_level == 0); + + /* Lookup the hash table entry with shared lock. */ + LWLockAcquire(pgss->lock, LW_SHARED); + + entry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL); + + /* Create new entry, if not present */ + if (!entry) + { + Size query_offset; + int gc_count; + bool stored; + bool do_gc; + bool sticky = true; + int extras_len; + + if (!edb_extracted) { + /* Try extract from the context of plan/execute. + * This is usually happening after a stats reset. + */ + if ((info = edbss_extract_stmt_info(query, query_len)) != NULL) { + /* We should just get the same queryId again + * as we extracted before the reset in post_parse. + */ + if (info->id.query_id != queryId) + goto done; + query = info->query; + query_len = info->query_len; + id = &info->id.uuid; + stmt_type = info->stmt_type; + extras = info->extras; + } else if (!edbss_track_unrecognized) { + /* skip unrecognized statements unless we're told not to */ + goto done; + } else { + sticky = jstate != NULL; + } + } + + /* + * Create a new, normalized query string if caller asked. We don't + * need to hold the lock while doing this work. (Note: in any case, + * it's possible that someone else creates a duplicate hashtable entry + * in the interval where we don't hold the lock below. That case is + * handled by entry_alloc.) + */ + if (jstate) + { + LWLockRelease(pgss->lock); + norm_query = generate_normalized_query(jstate, query, + query_location, + &query_len); + LWLockAcquire(pgss->lock, LW_SHARED); + } + + extras_len = extras == NULL ? 0 : VARSIZE(JsonbPGetDatum(extras)); + + /* Append new query text to file with only shared lock held */ + stored = qtext_store(norm_query ? norm_query : query, query_len, extras, extras_len, + &query_offset, &gc_count); + + /* + * Determine whether we need to garbage collect external query texts + * while the shared lock is still held. This micro-optimization + * avoids taking the time to decide this while holding exclusive lock. + */ + do_gc = need_gc_qtexts(); + + /* Need exclusive lock to make a new hashtable entry - promote */ + LWLockRelease(pgss->lock); + LWLockAcquire(pgss->lock, LW_EXCLUSIVE); + + /* + * A garbage collection may have occurred while we weren't holding the + * lock. In the unlikely event that this happens, the query text we + * stored above will have been garbage collected, so write it again. + * This should be infrequent enough that doing it while holding + * exclusive lock isn't a performance problem. + */ + if (!stored || pgss->gc_count != gc_count) + stored = qtext_store(norm_query ? norm_query : query, query_len, + extras, extras_len, + &query_offset, NULL); + + /* If we failed to write to the text file, give up */ + if (!stored) + goto done; + + /* OK to create a new hashtable entry */ + entry = entry_alloc(&key, query_offset, query_len, encoding, + sticky, id, stmt_type, extras_len); + + /* If needed, perform garbage collection while exclusive lock held */ + if (do_gc) + gc_qtexts(); + } + + /* Increment the counts, except when jstate is not NULL */ + if (!edb_extracted) + { + Assert(kind == PGSS_PLAN || kind == PGSS_EXEC); + + /* + * Grab the spinlock while updating the counters (see comment about + * locking rules at the head of the file) + */ + SpinLockAcquire(&entry->mutex); + + /* "Unstick" entry if it was previously sticky */ + if (IS_STICKY(entry->counters)) + entry->counters.usage = USAGE_INIT; + + entry->counters.calls[kind] += 1; + entry->counters.total_time[kind] += total_time; + + if (entry->counters.calls[kind] == 1) + { + entry->counters.min_time[kind] = total_time; + entry->counters.max_time[kind] = total_time; + entry->counters.mean_time[kind] = total_time; + } + else + { + /* + * Welford's method for accurately computing variance. See + * + */ + double old_mean = entry->counters.mean_time[kind]; + + entry->counters.mean_time[kind] += + (total_time - old_mean) / entry->counters.calls[kind]; + entry->counters.sum_var_time[kind] += + (total_time - old_mean) * (total_time - entry->counters.mean_time[kind]); + + /* + * Calculate min and max time. min = 0 and max = 0 means that the + * min/max statistics were reset + */ + if (entry->counters.min_time[kind] == 0 + && entry->counters.max_time[kind] == 0) + { + entry->counters.min_time[kind] = total_time; + entry->counters.max_time[kind] = total_time; + } + else + { + if (entry->counters.min_time[kind] > total_time) + entry->counters.min_time[kind] = total_time; + if (entry->counters.max_time[kind] < total_time) + entry->counters.max_time[kind] = total_time; + } + } + entry->counters.rows += rows; + entry->counters.shared_blks_hit += bufusage->shared_blks_hit; + entry->counters.shared_blks_read += bufusage->shared_blks_read; + entry->counters.shared_blks_dirtied += bufusage->shared_blks_dirtied; + entry->counters.shared_blks_written += bufusage->shared_blks_written; + entry->counters.local_blks_hit += bufusage->local_blks_hit; + entry->counters.local_blks_read += bufusage->local_blks_read; + entry->counters.local_blks_dirtied += bufusage->local_blks_dirtied; + entry->counters.local_blks_written += bufusage->local_blks_written; + entry->counters.temp_blks_read += bufusage->temp_blks_read; + entry->counters.temp_blks_written += bufusage->temp_blks_written; +#if PG_VERSION_NUM >= 170000 + entry->counters.shared_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->shared_blk_read_time); + entry->counters.shared_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->shared_blk_write_time); + entry->counters.local_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->local_blk_read_time); + entry->counters.local_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->local_blk_write_time); +#else + entry->counters.shared_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->blk_read_time); + entry->counters.shared_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->blk_write_time); +#endif + entry->counters.temp_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->temp_blk_read_time); + entry->counters.temp_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->temp_blk_write_time); + entry->counters.usage += USAGE_EXEC(total_time); + entry->counters.wal_records += walusage->wal_records; + entry->counters.wal_fpi += walusage->wal_fpi; + entry->counters.wal_bytes += walusage->wal_bytes; + if (jitusage) + { + entry->counters.jit_functions += jitusage->created_functions; + entry->counters.jit_generation_time += INSTR_TIME_GET_MILLISEC(jitusage->generation_counter); + +#if PG_VERSION_NUM >= 170000 + if (INSTR_TIME_GET_MILLISEC(jitusage->deform_counter)) + entry->counters.jit_deform_count++; + entry->counters.jit_deform_time += INSTR_TIME_GET_MILLISEC(jitusage->deform_counter); +#endif + + if (INSTR_TIME_GET_MILLISEC(jitusage->inlining_counter)) + entry->counters.jit_inlining_count++; + entry->counters.jit_inlining_time += INSTR_TIME_GET_MILLISEC(jitusage->inlining_counter); + + if (INSTR_TIME_GET_MILLISEC(jitusage->optimization_counter)) + entry->counters.jit_optimization_count++; + entry->counters.jit_optimization_time += INSTR_TIME_GET_MILLISEC(jitusage->optimization_counter); + + if (INSTR_TIME_GET_MILLISEC(jitusage->emission_counter)) + entry->counters.jit_emission_count++; + entry->counters.jit_emission_time += INSTR_TIME_GET_MILLISEC(jitusage->emission_counter); + } + + /* parallel worker counters */ + entry->counters.parallel_workers_to_launch += parallel_workers_to_launch; + entry->counters.parallel_workers_launched += parallel_workers_launched; + + SpinLockRelease(&entry->mutex); + } + +done: + LWLockRelease(pgss->lock); + + /* We postpone this clean-up until we're out of the lock */ + if (norm_query) + pfree(norm_query); + + if (info) + edbss_free_stmt_info(info); +} + +/* + * Reset statement statistics corresponding to userid, dbid, and queryid. + */ + +Datum +edb_stat_statements_reset(PG_FUNCTION_ARGS) +{ + Oid userid; + ArrayType *dbids_array; + Datum *dbids; + int dbids_len; + uint64 queryid; + bool minmax_only; + + userid = PG_GETARG_OID(0); + dbids_array = PG_GETARG_ARRAYTYPE_P(1); + queryid = (uint64) PG_GETARG_INT64(2); + minmax_only = PG_GETARG_BOOL(3); + + deconstruct_array_builtin(dbids_array, OIDOID, &dbids, NULL, &dbids_len); + + PG_RETURN_TIMESTAMPTZ(entry_reset(userid, dbids, dbids_len, queryid, minmax_only)); +} + +/* Number of output arguments (columns) for various API versions */ +#define PG_STAT_STATEMENTS_COLS_V1_0 54 +#define PG_STAT_STATEMENTS_COLS 54 /* maximum of above */ + +/* + * Retrieve statement statistics. + * + * The SQL API of this function has changed multiple times, and will likely + * do so again in future. To support the case where a newer version of this + * loadable module is being used with an old SQL declaration of the function, + * we continue to support the older API versions. For 1.2 and later, the + * expected API version is identified by embedding it in the C name of the + * function. Unfortunately we weren't bright enough to do that for 1.1. + */ +Datum +edb_stat_statements(PG_FUNCTION_ARGS) +{ + bool showtext = PG_GETARG_BOOL(0); + + edb_stat_statements_internal(fcinfo, PGSS_V1_0, showtext); + + return (Datum) 0; +} + +/* Common code for all versions of edb_stat_statements() */ +static void +edb_stat_statements_internal(FunctionCallInfo fcinfo, + pgssVersion api_version, + bool showtext) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + Oid userid = GetUserId(); + bool is_allowed_role = false; + char *qbuffer = NULL; + Size qbuffer_size = 0; + Size extent = 0; + int gc_count = 0; + HASH_SEQ_STATUS hash_seq; + pgssEntry *entry; + + /* + * Superusers or roles with the privileges of pg_read_all_stats members + * are allowed + */ + is_allowed_role = has_privs_of_role(userid, ROLE_PG_READ_ALL_STATS); + + /* hash table must exist already */ + if (!pgss || !pgss_hash) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("edb_stat_statements must be loaded via \"shared_preload_libraries\""))); + + InitMaterializedSRF(fcinfo, 0); + + /* + * Check we have the expected number of output arguments. Aside from + * being a good safety check, we need a kluge here to detect API version + * 1.1, which was wedged into the code in an ill-considered way. + */ + switch (rsinfo->setDesc->natts) + { + case PG_STAT_STATEMENTS_COLS_V1_0: + if (api_version != PGSS_V1_0) + elog(ERROR, "incorrect number of output arguments"); + break; + default: + elog(ERROR, "incorrect number of output arguments"); + } + + /* + * We'd like to load the query text file (if needed) while not holding any + * lock on pgss->lock. In the worst case we'll have to do this again + * after we have the lock, but it's unlikely enough to make this a win + * despite occasional duplicated work. We need to reload if anybody + * writes to the file (either a retail qtext_store(), or a garbage + * collection) between this point and where we've gotten shared lock. If + * a qtext_store is actually in progress when we look, we might as well + * skip the speculative load entirely. + */ + if (showtext) + { + int n_writers; + + /* Take the mutex so we can examine variables */ + SpinLockAcquire(&pgss->mutex); + extent = pgss->extent; + n_writers = pgss->n_writers; + gc_count = pgss->gc_count; + SpinLockRelease(&pgss->mutex); + + /* No point in loading file now if there are active writers */ + if (n_writers == 0) + qbuffer = qtext_load_file(&qbuffer_size); + } + + /* + * Get shared lock, load or reload the query text file if we must, and + * iterate over the hashtable entries. + * + * With a large hash table, we might be holding the lock rather longer + * than one could wish. However, this only blocks creation of new hash + * table entries, and the larger the hash table the less likely that is to + * be needed. So we can hope this is okay. Perhaps someday we'll decide + * we need to partition the hash table to limit the time spent holding any + * one lock. + */ + LWLockAcquire(pgss->lock, LW_SHARED); + + if (showtext) + { + /* + * Here it is safe to examine extent and gc_count without taking the + * mutex. Note that although other processes might change + * pgss->extent just after we look at it, the strings they then write + * into the file cannot yet be referenced in the hashtable, so we + * don't care whether we see them or not. + * + * If qtext_load_file fails, we just press on; we'll return NULL for + * every query text. + */ + if (qbuffer == NULL || + pgss->extent != extent || + pgss->gc_count != gc_count) + { + free(qbuffer); + qbuffer = qtext_load_file(&qbuffer_size); + } + } + + hash_seq_init(&hash_seq, pgss_hash); + while ((entry = hash_seq_search(&hash_seq)) != NULL) + { + Datum values[PG_STAT_STATEMENTS_COLS]; + bool nulls[PG_STAT_STATEMENTS_COLS]; + int i = 0; + Counters tmp; + double stddev; + int64 queryid = entry->key.queryid; + TimestampTz stats_since; + TimestampTz minmax_stats_since; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[i++] = ObjectIdGetDatum(entry->key.userid); + values[i++] = ObjectIdGetDatum(entry->key.dbid); + values[i++] = BoolGetDatum(entry->key.toplevel); + + if (is_allowed_role || entry->key.userid == userid) + { + values[i++] = Int64GetDatumFast(queryid); + + if (showtext) + { + char *qstr = qtext_fetch(entry->query_offset, + entry->query_len + entry->extras_len, + qbuffer, + qbuffer_size); + + if (qstr) + { + char *enc; + + enc = pg_any_to_server(qstr + entry->extras_len, + entry->query_len, + entry->encoding); + + values[i++] = CStringGetTextDatum(enc); + + // The "extras" Jsonb varlena datum + if (entry->extras_len > 0) + values[i++] = PointerGetDatum(qstr); + else + nulls[i++] = true; + + if (enc != qstr + entry->extras_len) + pfree(enc); + } + else + { + /* Just return a null if we fail to find the text */ + nulls[i++] = true; + + /* null extras */ + nulls[i++] = true; + } + } + else + { + /* Query text not requested */ + nulls[i++] = true; + + /* null extras */ + nulls[i++] = true; + } + } + else + { + /* Don't show queryid */ + nulls[i++] = true; + + /* + * Don't show query text, but hint as to the reason for not doing + * so if it was requested + */ + if (showtext) + values[i++] = CStringGetTextDatum(""); + else + nulls[i++] = true; + + /* null extras */ + nulls[i++] = true; + } + + if (memcmp(&entry->id, &zero_uuid, sizeof(zero_uuid)) == 0) + nulls[i++] = true; + else + values[i++] = UUIDPGetDatum(&entry->id); + + if (entry->stmt_type == 0) + nulls[i++] = true; + else + values[i++] = Int16GetDatum(entry->stmt_type); + + /* copy counters to a local variable to keep locking time short */ + SpinLockAcquire(&entry->mutex); + tmp = entry->counters; + stats_since = entry->stats_since; + minmax_stats_since = entry->minmax_stats_since; + SpinLockRelease(&entry->mutex); + + /* Skip entry if unexecuted (ie, it's a pending "sticky" entry) */ + if (IS_STICKY(tmp)) + continue; + + /* Note that we rely on PGSS_PLAN being 0 and PGSS_EXEC being 1. */ + for (int kind = 0; kind < PGSS_NUMKIND; kind++) + { + values[i++] = Int64GetDatumFast(tmp.calls[kind]); + values[i++] = Float8GetDatumFast(tmp.total_time[kind]); + values[i++] = Float8GetDatumFast(tmp.min_time[kind]); + values[i++] = Float8GetDatumFast(tmp.max_time[kind]); + values[i++] = Float8GetDatumFast(tmp.mean_time[kind]); + + /* + * Note we are calculating the population variance here, not + * the sample variance, as we have data for the whole + * population, so Bessel's correction is not used, and we + * don't divide by tmp.calls - 1. + */ + if (tmp.calls[kind] > 1) + stddev = sqrt(tmp.sum_var_time[kind] / tmp.calls[kind]); + else + stddev = 0.0; + values[i++] = Float8GetDatumFast(stddev); + } + values[i++] = Int64GetDatumFast(tmp.rows); + values[i++] = Int64GetDatumFast(tmp.shared_blks_hit); + values[i++] = Int64GetDatumFast(tmp.shared_blks_read); + values[i++] = Int64GetDatumFast(tmp.shared_blks_dirtied); + values[i++] = Int64GetDatumFast(tmp.shared_blks_written); + values[i++] = Int64GetDatumFast(tmp.local_blks_hit); + values[i++] = Int64GetDatumFast(tmp.local_blks_read); + values[i++] = Int64GetDatumFast(tmp.local_blks_dirtied); + values[i++] = Int64GetDatumFast(tmp.local_blks_written); + values[i++] = Int64GetDatumFast(tmp.temp_blks_read); + values[i++] = Int64GetDatumFast(tmp.temp_blks_written); + values[i++] = Float8GetDatumFast(tmp.shared_blk_read_time); + values[i++] = Float8GetDatumFast(tmp.shared_blk_write_time); + values[i++] = Float8GetDatumFast(tmp.local_blk_read_time); + values[i++] = Float8GetDatumFast(tmp.local_blk_write_time); + values[i++] = Float8GetDatumFast(tmp.temp_blk_read_time); + values[i++] = Float8GetDatumFast(tmp.temp_blk_write_time); + { + char buf[256]; + Datum wal_bytes; + + values[i++] = Int64GetDatumFast(tmp.wal_records); + values[i++] = Int64GetDatumFast(tmp.wal_fpi); + + snprintf(buf, sizeof buf, UINT64_FORMAT, tmp.wal_bytes); + + /* Convert to numeric. */ + wal_bytes = DirectFunctionCall3(numeric_in, + CStringGetDatum(buf), + ObjectIdGetDatum(0), + Int32GetDatum(-1)); + values[i++] = wal_bytes; + } + values[i++] = Int64GetDatumFast(tmp.jit_functions); + values[i++] = Float8GetDatumFast(tmp.jit_generation_time); + values[i++] = Int64GetDatumFast(tmp.jit_inlining_count); + values[i++] = Float8GetDatumFast(tmp.jit_inlining_time); + values[i++] = Int64GetDatumFast(tmp.jit_optimization_count); + values[i++] = Float8GetDatumFast(tmp.jit_optimization_time); + values[i++] = Int64GetDatumFast(tmp.jit_emission_count); + values[i++] = Float8GetDatumFast(tmp.jit_emission_time); + values[i++] = Int64GetDatumFast(tmp.jit_deform_count); + values[i++] = Float8GetDatumFast(tmp.jit_deform_time); + values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch); + values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched); + values[i++] = TimestampTzGetDatum(stats_since); + values[i++] = TimestampTzGetDatum(minmax_stats_since); + + Assert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 : + -1 /* fail if you forget to update this assert */ )); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + + LWLockRelease(pgss->lock); + + free(qbuffer); +} + +/* Number of output arguments (columns) for edb_stat_statements_info */ +#define PG_STAT_STATEMENTS_INFO_COLS 2 + +/* + * Return statistics of edb_stat_statements. + */ +Datum +edb_stat_statements_info(PG_FUNCTION_ARGS) +{ + pgssGlobalStats stats; + TupleDesc tupdesc; + Datum values[PG_STAT_STATEMENTS_INFO_COLS] = {0}; + bool nulls[PG_STAT_STATEMENTS_INFO_COLS] = {0}; + + if (!pgss || !pgss_hash) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("edb_stat_statements must be loaded via \"shared_preload_libraries\""))); + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + /* Read global statistics for edb_stat_statements */ + SpinLockAcquire(&pgss->mutex); + stats = pgss->stats; + SpinLockRelease(&pgss->mutex); + + values[0] = Int64GetDatum(stats.dealloc); + values[1] = TimestampTzGetDatum(stats.stats_reset); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * Convert uuid to bigint as queryid. + */ +Datum +edb_stat_queryid(PG_FUNCTION_ARGS) { + union { + pg_uuid_t uuid; + uint64 id; + } id; + id.uuid = *PG_GETARG_UUID_P(0); + return UInt64GetDatum(id.id); +} + +/* + * Estimate shared memory space needed. + */ +static Size +pgss_memsize(void) +{ + Size size; + + size = MAXALIGN(sizeof(pgssSharedState)); + size = add_size(size, hash_estimate_size(pgss_max, sizeof(pgssEntry))); + + return size; +} + +/* + * Allocate a new hashtable entry. + * caller must hold an exclusive lock on pgss->lock + * + * "query" need not be null-terminated; we rely on query_len instead + * + * If "sticky" is true, make the new entry artificially sticky so that it will + * probably still be there when the query finishes execution. We do this by + * giving it a median usage value rather than the normal value. (Strictly + * speaking, query strings are normalized on a best effort basis, though it + * would be difficult to demonstrate this even under artificial conditions.) + * + * Note: despite needing exclusive lock, it's not an error for the target + * entry to already exist. This is because pgss_store releases and + * reacquires lock after failing to find a match; so someone else could + * have made the entry while we waited to get exclusive lock. + */ +static pgssEntry * +entry_alloc(pgssHashKey *key, Size query_offset, int query_len, int encoding, + bool sticky, pg_uuid_t *id, EdbStmtType stmt_type, int extras_len) +{ + pgssEntry *entry; + bool found; + + /* Make space if needed */ + while (hash_get_num_entries(pgss_hash) >= pgss_max) + entry_dealloc(); + + /* Find or create an entry with desired hash code */ + entry = (pgssEntry *) hash_search(pgss_hash, key, HASH_ENTER, &found); + + if (!found) + { + /* New entry, initialize it */ + + /* reset the statistics */ + memset(&entry->counters, 0, sizeof(Counters)); + /* set the appropriate initial usage count */ + entry->counters.usage = sticky ? pgss->cur_median_usage : USAGE_INIT; + /* re-initialize the mutex each time ... we assume no one using it */ + SpinLockInit(&entry->mutex); + /* ... and don't forget the query text metadata */ + Assert(query_len >= 0); + entry->query_offset = query_offset; + entry->query_len = query_len; + entry->encoding = encoding; + entry->stats_since = GetCurrentTimestamp(); + entry->minmax_stats_since = entry->stats_since; + if (id != NULL) + entry->id = *id; + entry->stmt_type = stmt_type; + entry->extras_len = extras_len; + } + + return entry; +} + +/* + * qsort comparator for sorting into increasing usage order + */ +static int +entry_cmp(const void *lhs, const void *rhs) +{ + double l_usage = (*(pgssEntry *const *) lhs)->counters.usage; + double r_usage = (*(pgssEntry *const *) rhs)->counters.usage; + + if (l_usage < r_usage) + return -1; + else if (l_usage > r_usage) + return +1; + else + return 0; +} + +/* + * Deallocate least-used entries. + * + * Caller must hold an exclusive lock on pgss->lock. + */ +static void +entry_dealloc(void) +{ + HASH_SEQ_STATUS hash_seq; + pgssEntry **entries; + pgssEntry *entry; + int nvictims; + int i; + Size tottextlen; + int nvalidtexts; + + /* + * Sort entries by usage and deallocate USAGE_DEALLOC_PERCENT of them. + * While we're scanning the table, apply the decay factor to the usage + * values, and update the mean query length. + * + * Note that the mean query length is almost immediately obsolete, since + * we compute it before not after discarding the least-used entries. + * Hopefully, that doesn't affect the mean too much; it doesn't seem worth + * making two passes to get a more current result. Likewise, the new + * cur_median_usage includes the entries we're about to zap. + */ + + entries = palloc(hash_get_num_entries(pgss_hash) * sizeof(pgssEntry *)); + + i = 0; + tottextlen = 0; + nvalidtexts = 0; + + hash_seq_init(&hash_seq, pgss_hash); + while ((entry = hash_seq_search(&hash_seq)) != NULL) + { + entries[i++] = entry; + /* "Sticky" entries get a different usage decay rate. */ + if (IS_STICKY(entry->counters)) + entry->counters.usage *= STICKY_DECREASE_FACTOR; + else + entry->counters.usage *= USAGE_DECREASE_FACTOR; + /* In the mean length computation, ignore dropped texts. */ + if (entry->query_len >= 0) + { + tottextlen += entry->query_len + 1; + nvalidtexts++; + } + } + + /* Sort into increasing order by usage */ + qsort(entries, i, sizeof(pgssEntry *), entry_cmp); + + /* Record the (approximate) median usage */ + if (i > 0) + pgss->cur_median_usage = entries[i / 2]->counters.usage; + /* Record the mean query length */ + if (nvalidtexts > 0) + pgss->mean_query_len = tottextlen / nvalidtexts; + else + pgss->mean_query_len = ASSUMED_LENGTH_INIT; + + /* Now zap an appropriate fraction of lowest-usage entries */ + nvictims = Max(10, i * USAGE_DEALLOC_PERCENT / 100); + nvictims = Min(nvictims, i); + + for (i = 0; i < nvictims; i++) + { + hash_search(pgss_hash, &entries[i]->key, HASH_REMOVE, NULL); + } + + pfree(entries); + + /* Increment the number of times entries are deallocated */ + SpinLockAcquire(&pgss->mutex); + pgss->stats.dealloc += 1; + SpinLockRelease(&pgss->mutex); +} + +/* + * Given a query string (not necessarily null-terminated), allocate a new + * entry in the external query text file and store the string there. + * + * If successful, returns true, and stores the new entry's offset in the file + * into *query_offset. Also, if gc_count isn't NULL, *gc_count is set to the + * number of garbage collections that have occurred so far. + * + * On failure, returns false. + * + * At least a shared lock on pgss->lock must be held by the caller, so as + * to prevent a concurrent garbage collection. Share-lock-holding callers + * should pass a gc_count pointer to obtain the number of garbage collections, + * so that they can recheck the count after obtaining exclusive lock to + * detect whether a garbage collection occurred (and removed this entry). + */ +static bool +qtext_store(const char *query, int query_len, + const Jsonb *extras, int extras_len, + Size *query_offset, int *gc_count) +{ + Size off; + int fd; + + /* + * We use a spinlock to protect extent/n_writers/gc_count, so that + * multiple processes may execute this function concurrently. + */ + SpinLockAcquire(&pgss->mutex); + off = pgss->extent; + pgss->extent += query_len + extras_len + 1; + pgss->n_writers++; + if (gc_count) + *gc_count = pgss->gc_count; + SpinLockRelease(&pgss->mutex); + + *query_offset = off; + + /* + * Don't allow the file to grow larger than what qtext_load_file can + * (theoretically) handle. This has been seen to be reachable on 32-bit + * platforms. + */ + if (unlikely(query_len + extras_len >= MaxAllocHugeSize - off)) + { + errno = EFBIG; /* not quite right, but it'll do */ + fd = -1; + goto error; + } + + /* Now write the data into the successfully-reserved part of the file */ + fd = OpenTransientFile(PGSS_TEXT_FILE, O_RDWR | O_CREAT | PG_BINARY); + if (fd < 0) + goto error; + + if (extras_len > 0 && pg_pwrite(fd, extras, extras_len, off) != extras_len) + goto error; + if (pg_pwrite(fd, query, query_len, off + extras_len) != query_len) + goto error; + if (pg_pwrite(fd, "\0", 1, off + extras_len + query_len) != 1) + goto error; + + CloseTransientFile(fd); + + /* Mark our write complete */ + SpinLockAcquire(&pgss->mutex); + pgss->n_writers--; + SpinLockRelease(&pgss->mutex); + + return true; + +error: + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + PGSS_TEXT_FILE))); + + if (fd >= 0) + CloseTransientFile(fd); + + /* Mark our write complete */ + SpinLockAcquire(&pgss->mutex); + pgss->n_writers--; + SpinLockRelease(&pgss->mutex); + + return false; +} + +/* + * Read the external query text file into a malloc'd buffer. + * + * Returns NULL (without throwing an error) if unable to read, eg + * file not there or insufficient memory. + * + * On success, the buffer size is also returned into *buffer_size. + * + * This can be called without any lock on pgss->lock, but in that case + * the caller is responsible for verifying that the result is sane. + */ +static char * +qtext_load_file(Size *buffer_size) +{ + char *buf; + int fd; + struct stat stat; + Size nread; + + fd = OpenTransientFile(PGSS_TEXT_FILE, O_RDONLY | PG_BINARY); + if (fd < 0) + { + if (errno != ENOENT) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", + PGSS_TEXT_FILE))); + return NULL; + } + + /* Get file length */ + if (fstat(fd, &stat)) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + PGSS_TEXT_FILE))); + CloseTransientFile(fd); + return NULL; + } + + /* Allocate buffer; beware that off_t might be wider than size_t */ + if (stat.st_size <= MaxAllocHugeSize) + buf = (char *) malloc(stat.st_size); + else + buf = NULL; + if (buf == NULL) + { + ereport(LOG, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Could not allocate enough memory to read file \"%s\".", + PGSS_TEXT_FILE))); + CloseTransientFile(fd); + return NULL; + } + + /* + * OK, slurp in the file. Windows fails if we try to read more than + * INT_MAX bytes at once, and other platforms might not like that either, + * so read a very large file in 1GB segments. + */ + nread = 0; + while (nread < stat.st_size) + { + int toread = Min(1024 * 1024 * 1024, stat.st_size - nread); + + /* + * If we get a short read and errno doesn't get set, the reason is + * probably that garbage collection truncated the file since we did + * the fstat(), so we don't log a complaint --- but we don't return + * the data, either, since it's most likely corrupt due to concurrent + * writes from garbage collection. + */ + errno = 0; + if (read(fd, buf + nread, toread) != toread) + { + if (errno) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", + PGSS_TEXT_FILE))); + free(buf); + CloseTransientFile(fd); + return NULL; + } + nread += toread; + } + + if (CloseTransientFile(fd) != 0) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", PGSS_TEXT_FILE))); + + *buffer_size = nread; + return buf; +} + +/* + * Locate a query text in the file image previously read by qtext_load_file(). + * + * We validate the given offset/length, and return NULL if bogus. Otherwise, + * the result points to a null-terminated string within the buffer. + */ +static char * +qtext_fetch(Size query_offset, int query_len, + char *buffer, Size buffer_size) +{ + /* File read failed? */ + if (buffer == NULL) + return NULL; + /* Bogus offset/length? */ + if (query_len < 0 || + query_offset + query_len >= buffer_size) + return NULL; + /* As a further sanity check, make sure there's a trailing null */ + if (buffer[query_offset + query_len] != '\0') + return NULL; + /* Looks OK */ + return buffer + query_offset; +} + +/* + * Do we need to garbage-collect the external query text file? + * + * Caller should hold at least a shared lock on pgss->lock. + */ +static bool +need_gc_qtexts(void) +{ + Size extent; + + /* Read shared extent pointer */ + SpinLockAcquire(&pgss->mutex); + extent = pgss->extent; + SpinLockRelease(&pgss->mutex); + + /* + * Don't proceed if file does not exceed 512 bytes per possible entry. + * + * Here and in the next test, 32-bit machines have overflow hazards if + * pgss_max and/or mean_query_len are large. Force the multiplications + * and comparisons to be done in uint64 arithmetic to forestall trouble. + */ + if ((uint64) extent < (uint64) 512 * pgss_max) + return false; + + /* + * Don't proceed if file is less than about 50% bloat. Nothing can or + * should be done in the event of unusually large query texts accounting + * for file's large size. We go to the trouble of maintaining the mean + * query length in order to prevent garbage collection from thrashing + * uselessly. + */ + if ((uint64) extent < (uint64) pgss->mean_query_len * pgss_max * 2) + return false; + + return true; +} + +/* + * Garbage-collect orphaned query texts in external file. + * + * This won't be called often in the typical case, since it's likely that + * there won't be too much churn, and besides, a similar compaction process + * occurs when serializing to disk at shutdown or as part of resetting. + * Despite this, it seems prudent to plan for the edge case where the file + * becomes unreasonably large, with no other method of compaction likely to + * occur in the foreseeable future. + * + * The caller must hold an exclusive lock on pgss->lock. + * + * At the first sign of trouble we unlink the query text file to get a clean + * slate (although existing statistics are retained), rather than risk + * thrashing by allowing the same problem case to recur indefinitely. + */ +static void +gc_qtexts(void) +{ + char *qbuffer; + Size qbuffer_size; + FILE *qfile = NULL; + HASH_SEQ_STATUS hash_seq; + pgssEntry *entry; + Size extent; + int nentries; + + /* + * When called from pgss_store, some other session might have proceeded + * with garbage collection in the no-lock-held interim of lock strength + * escalation. Check once more that this is actually necessary. + */ + if (!need_gc_qtexts()) + return; + + /* + * Load the old texts file. If we fail (out of memory, for instance), + * invalidate query texts. Hopefully this is rare. It might seem better + * to leave things alone on an OOM failure, but the problem is that the + * file is only going to get bigger; hoping for a future non-OOM result is + * risky and can easily lead to complete denial of service. + */ + qbuffer = qtext_load_file(&qbuffer_size); + if (qbuffer == NULL) + goto gc_fail; + + /* + * We overwrite the query texts file in place, so as to reduce the risk of + * an out-of-disk-space failure. Since the file is guaranteed not to get + * larger, this should always work on traditional filesystems; though we + * could still lose on copy-on-write filesystems. + */ + qfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W); + if (qfile == NULL) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + PGSS_TEXT_FILE))); + goto gc_fail; + } + + extent = 0; + nentries = 0; + + hash_seq_init(&hash_seq, pgss_hash); + while ((entry = hash_seq_search(&hash_seq)) != NULL) + { + int query_len = entry->query_len + entry->extras_len; + char *qry = qtext_fetch(entry->query_offset, + query_len, + qbuffer, + qbuffer_size); + + if (qry == NULL) + { + /* Trouble ... drop the text */ + entry->query_offset = 0; + entry->query_len = -1; + entry->extras_len = 0; + /* entry will not be counted in mean query length computation */ + continue; + } + + if (fwrite(qry, 1, query_len + 1, qfile) != query_len + 1) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + PGSS_TEXT_FILE))); + hash_seq_term(&hash_seq); + goto gc_fail; + } + + entry->query_offset = extent; + extent += query_len + 1; + nentries++; + } + + /* + * Truncate away any now-unused space. If this fails for some odd reason, + * we log it, but there's no need to fail. + */ + if (ftruncate(fileno(qfile), extent) != 0) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not truncate file \"%s\": %m", + PGSS_TEXT_FILE))); + + if (FreeFile(qfile)) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + PGSS_TEXT_FILE))); + qfile = NULL; + goto gc_fail; + } + + elog(DEBUG1, "pgss gc of queries file shrunk size from %zu to %zu", + pgss->extent, extent); + + /* Reset the shared extent pointer */ + pgss->extent = extent; + + /* + * Also update the mean query length, to be sure that need_gc_qtexts() + * won't still think we have a problem. + */ + if (nentries > 0) + pgss->mean_query_len = extent / nentries; + else + pgss->mean_query_len = ASSUMED_LENGTH_INIT; + + free(qbuffer); + + /* + * OK, count a garbage collection cycle. (Note: even though we have + * exclusive lock on pgss->lock, we must take pgss->mutex for this, since + * other processes may examine gc_count while holding only the mutex. + * Also, we have to advance the count *after* we've rewritten the file, + * else other processes might not realize they read a stale file.) + */ + record_gc_qtexts(); + + return; + +gc_fail: + /* clean up resources */ + if (qfile) + FreeFile(qfile); + free(qbuffer); + + /* + * Since the contents of the external file are now uncertain, mark all + * hashtable entries as having invalid texts. + */ + hash_seq_init(&hash_seq, pgss_hash); + while ((entry = hash_seq_search(&hash_seq)) != NULL) + { + entry->query_offset = 0; + entry->query_len = -1; + entry->extras_len = 0; + } + + /* + * Destroy the query text file and create a new, empty one + */ + (void) unlink(PGSS_TEXT_FILE); + qfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W); + if (qfile == NULL) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not recreate file \"%s\": %m", + PGSS_TEXT_FILE))); + else + FreeFile(qfile); + + /* Reset the shared extent pointer */ + pgss->extent = 0; + + /* Reset mean_query_len to match the new state */ + pgss->mean_query_len = ASSUMED_LENGTH_INIT; + + /* + * Bump the GC count even though we failed. + * + * This is needed to make concurrent readers of file without any lock on + * pgss->lock notice existence of new version of file. Once readers + * subsequently observe a change in GC count with pgss->lock held, that + * forces a safe reopen of file. Writers also require that we bump here, + * of course. (As required by locking protocol, readers and writers don't + * trust earlier file contents until gc_count is found unchanged after + * pgss->lock acquired in shared or exclusive mode respectively.) + */ + record_gc_qtexts(); +} + +#define SINGLE_ENTRY_RESET(e) \ +if (e) { \ + if (minmax_only) { \ + /* When requested reset only min/max statistics of an entry */ \ + for (int kind = 0; kind < PGSS_NUMKIND; kind++) \ + { \ + e->counters.max_time[kind] = 0; \ + e->counters.min_time[kind] = 0; \ + } \ + e->minmax_stats_since = stats_reset; \ + } \ + else \ + { \ + /* Remove the key otherwise */ \ + hash_search(pgss_hash, &e->key, HASH_REMOVE, NULL); \ + num_remove++; \ + } \ +} + +/* + * Reset entries corresponding to parameters passed. + */ +static TimestampTz +entry_reset(Oid userid, const Datum *dbids, int dbids_len, uint64 queryid, bool minmax_only) +{ + HASH_SEQ_STATUS hash_seq; + pgssEntry *entry; + FILE *qfile; + long num_entries; + long num_remove = 0; + pgssHashKey key; + TimestampTz stats_reset; + + if (!pgss || !pgss_hash) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("edb_stat_statements must be loaded via \"shared_preload_libraries\""))); + + LWLockAcquire(pgss->lock, LW_EXCLUSIVE); + num_entries = hash_get_num_entries(pgss_hash); + + stats_reset = GetCurrentTimestamp(); + + if (userid != 0 && dbids_len == 1 && queryid != UINT64CONST(0)) + { + /* If all the parameters are available, use the fast path. */ + memset(&key, 0, sizeof(pgssHashKey)); + key.userid = userid; + key.dbid = DatumGetObjectId(dbids[0]); + key.queryid = queryid; + + /* + * Reset the entry if it exists, starting with the non-top-level + * entry. + */ + key.toplevel = false; + entry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL); + + SINGLE_ENTRY_RESET(entry); + + /* Also reset the top-level entry if it exists. */ + key.toplevel = true; + entry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL); + + SINGLE_ENTRY_RESET(entry); + } + else if (userid != 0 || dbids_len > 0 || queryid != UINT64CONST(0)) + { + /* Reset entries corresponding to valid parameters. */ + hash_seq_init(&hash_seq, pgss_hash); + if (dbids_len > 0) { + while ((entry = hash_seq_search(&hash_seq)) != NULL) { + for (int i = 0; i < dbids_len; i++) { + Oid dbid = DatumGetObjectId(dbids[i]); + if ((!userid || entry->key.userid == userid) && + (entry->key.dbid == dbid) && + (!queryid || entry->key.queryid == queryid)) { + SINGLE_ENTRY_RESET(entry); + break; + } + } + } + } else { + while ((entry = hash_seq_search(&hash_seq)) != NULL) { + if ((!userid || entry->key.userid == userid) && + (!queryid || entry->key.queryid == queryid)) { + SINGLE_ENTRY_RESET(entry); + } + } + } + } + else + { + /* Reset all entries. */ + hash_seq_init(&hash_seq, pgss_hash); + while ((entry = hash_seq_search(&hash_seq)) != NULL) + { + SINGLE_ENTRY_RESET(entry); + } + } + + /* All entries are removed? */ + if (num_entries != num_remove) + goto release_lock; + + /* + * Reset global statistics for edb_stat_statements since all entries are + * removed. + */ + SpinLockAcquire(&pgss->mutex); + pgss->stats.dealloc = 0; + pgss->stats.stats_reset = stats_reset; + SpinLockRelease(&pgss->mutex); + + /* + * Write new empty query file, perhaps even creating a new one to recover + * if the file was missing. + */ + qfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W); + if (qfile == NULL) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not create file \"%s\": %m", + PGSS_TEXT_FILE))); + goto done; + } + + /* If ftruncate fails, log it, but it's not a fatal problem */ + if (ftruncate(fileno(qfile), 0) != 0) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not truncate file \"%s\": %m", + PGSS_TEXT_FILE))); + + FreeFile(qfile); + +done: + pgss->extent = 0; + /* This counts as a query text garbage collection for our purposes */ + record_gc_qtexts(); + +release_lock: + LWLockRelease(pgss->lock); + + return stats_reset; +} + +/* + * Generate a normalized version of the query string that will be used to + * represent all similar queries. + * + * Note that the normalized representation may well vary depending on + * just which "equivalent" query is used to create the hashtable entry. + * We assume this is OK. + * + * If query_loc > 0, then "query" has been advanced by that much compared to + * the original string start, so we need to translate the provided locations + * to compensate. (This lets us avoid re-scanning statements before the one + * of interest, so it's worth doing.) + * + * *query_len_p contains the input string length, and is updated with + * the result string length on exit. The resulting string might be longer + * or shorter depending on what happens with replacement of constants. + * + * Returns a palloc'd string. + */ +static char * +generate_normalized_query(JumbleState *jstate, const char *query, + int query_loc, int *query_len_p) +{ + char *norm_query; + int query_len = *query_len_p; + int i, + norm_query_buflen, /* Space allowed for norm_query */ + len_to_wrt, /* Length (in bytes) to write */ + quer_loc = 0, /* Source query byte location */ + n_quer_loc = 0, /* Normalized query byte location */ + last_off = 0, /* Offset from start for previous tok */ + last_tok_len = 0; /* Length (in bytes) of that tok */ + + /* + * Get constants' lengths (core system only gives us locations). Note + * this also ensures the items are sorted by location. + */ + fill_in_constant_lengths(jstate, query, query_loc); + + /* + * Allow for $n symbols to be longer than the constants they replace. + * Constants must take at least one byte in text form, while a $n symbol + * certainly isn't more than 11 bytes, even if n reaches INT_MAX. We + * could refine that limit based on the max value of n for the current + * query, but it hardly seems worth any extra effort to do so. + */ + norm_query_buflen = query_len + jstate->clocations_count * 10; + + /* Allocate result buffer */ + norm_query = palloc(norm_query_buflen + 1); + + for (i = 0; i < jstate->clocations_count; i++) + { + int off, /* Offset from start for cur tok */ + tok_len; /* Length (in bytes) of that tok */ + + off = jstate->clocations[i].location; + /* Adjust recorded location if we're dealing with partial string */ + off -= query_loc; + + tok_len = jstate->clocations[i].length; + + if (tok_len < 0) + continue; /* ignore any duplicates */ + + /* Copy next chunk (what precedes the next constant) */ + len_to_wrt = off - last_off; + len_to_wrt -= last_tok_len; + + Assert(len_to_wrt >= 0); + memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); + n_quer_loc += len_to_wrt; + + /* And insert a param symbol in place of the constant token */ + n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d", + i + 1 + jstate->highest_extern_param_id); + + quer_loc = off + tok_len; + last_off = off; + last_tok_len = tok_len; + } + + /* + * We've copied up until the last ignorable constant. Copy over the + * remaining bytes of the original query string. + */ + len_to_wrt = query_len - quer_loc; + + Assert(len_to_wrt >= 0); + memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); + n_quer_loc += len_to_wrt; + + Assert(n_quer_loc <= norm_query_buflen); + norm_query[n_quer_loc] = '\0'; + + *query_len_p = n_quer_loc; + return norm_query; +} + +/* + * Given a valid SQL string and an array of constant-location records, + * fill in the textual lengths of those constants. + * + * The constants may use any allowed constant syntax, such as float literals, + * bit-strings, single-quoted strings and dollar-quoted strings. This is + * accomplished by using the public API for the core scanner. + * + * It is the caller's job to ensure that the string is a valid SQL statement + * with constants at the indicated locations. Since in practice the string + * has already been parsed, and the locations that the caller provides will + * have originated from within the authoritative parser, this should not be + * a problem. + * + * Duplicate constant pointers are possible, and will have their lengths + * marked as '-1', so that they are later ignored. (Actually, we assume the + * lengths were initialized as -1 to start with, and don't change them here.) + * + * If query_loc > 0, then "query" has been advanced by that much compared to + * the original string start, so we need to translate the provided locations + * to compensate. (This lets us avoid re-scanning statements before the one + * of interest, so it's worth doing.) + * + * N.B. There is an assumption that a '-' character at a Const location begins + * a negative numeric constant. This precludes there ever being another + * reason for a constant to start with a '-'. + */ +static void +fill_in_constant_lengths(JumbleState *jstate, const char *query, + int query_loc) +{ + LocationLen *locs; + core_yyscan_t yyscanner; + core_yy_extra_type yyextra; + core_YYSTYPE yylval; + YYLTYPE yylloc; + int last_loc = -1; + int i; + + /* + * Sort the records by location so that we can process them in order while + * scanning the query text. + */ + if (jstate->clocations_count > 1) + qsort(jstate->clocations, jstate->clocations_count, + sizeof(LocationLen), comp_location); + locs = jstate->clocations; + + /* initialize the flex scanner --- should match raw_parser() */ + yyscanner = scanner_init(query, + &yyextra, + &ScanKeywords, + ScanKeywordTokens); + + /* we don't want to re-emit any escape string warnings */ + yyextra.escape_string_warning = false; + + /* Search for each constant, in sequence */ + for (i = 0; i < jstate->clocations_count; i++) + { + int loc = locs[i].location; + int tok; + + /* Adjust recorded location if we're dealing with partial string */ + loc -= query_loc; + + Assert(loc >= 0); + + if (loc <= last_loc) + continue; /* Duplicate constant, ignore */ + + /* Lex tokens until we find the desired constant */ + for (;;) + { + tok = core_yylex(&yylval, &yylloc, yyscanner); + + /* We should not hit end-of-string, but if we do, behave sanely */ + if (tok == 0) + break; /* out of inner for-loop */ + + /* + * We should find the token position exactly, but if we somehow + * run past it, work with that. + */ + if (yylloc >= loc) + { + if (query[loc] == '-') + { + /* + * It's a negative value - this is the one and only case + * where we replace more than a single token. + * + * Do not compensate for the core system's special-case + * adjustment of location to that of the leading '-' + * operator in the event of a negative constant. It is + * also useful for our purposes to start from the minus + * symbol. In this way, queries like "select * from foo + * where bar = 1" and "select * from foo where bar = -2" + * will have identical normalized query strings. + */ + tok = core_yylex(&yylval, &yylloc, yyscanner); + if (tok == 0) + break; /* out of inner for-loop */ + } + + /* + * We now rely on the assumption that flex has placed a zero + * byte after the text of the current token in scanbuf. + */ + locs[i].length = strlen(yyextra.scanbuf + loc); + break; /* out of inner for-loop */ + } + } + + /* If we hit end-of-string, give up, leaving remaining lengths -1 */ + if (tok == 0) + break; + + last_loc = loc; + } + + scanner_finish(yyscanner); +} + +/* + * comp_location: comparator for qsorting LocationLen structs by location + */ +static int +comp_location(const void *a, const void *b) +{ + int l = ((const LocationLen *) a)->location; + int r = ((const LocationLen *) b)->location; + +#if PG_VERSION_NUM >= 170000 + return pg_cmp_s32(l, r); +#else + if (l < r) + return -1; + else if (l > r) + return +1; + else + return 0; +#endif +} diff --git a/edb_stat_statements/edb_stat_statements.control b/edb_stat_statements/edb_stat_statements.control new file mode 100644 index 00000000000..4884103987f --- /dev/null +++ b/edb_stat_statements/edb_stat_statements.control @@ -0,0 +1,5 @@ +# edb_stat_statements extension +comment = 'track planning and execution statistics of all EdgeDB queries executed' +default_version = '1.0' +module_pathname = '$libdir/edb_stat_statements' +relocatable = true diff --git a/edb_stat_statements/expected/cleanup.out b/edb_stat_statements/expected/cleanup.out new file mode 100644 index 00000000000..03e40380b87 --- /dev/null +++ b/edb_stat_statements/expected/cleanup.out @@ -0,0 +1 @@ +DROP EXTENSION edb_stat_statements; diff --git a/edb_stat_statements/expected/cursors.out b/edb_stat_statements/expected/cursors.out new file mode 100644 index 00000000000..677ab63a506 --- /dev/null +++ b/edb_stat_statements/expected/cursors.out @@ -0,0 +1,70 @@ +-- +-- Cursors +-- +-- These tests require track_utility to be enabled. +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- DECLARE +-- SELECT is normalized. +DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 1; +CLOSE cursor_stats_1; +DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2; +CLOSE cursor_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------- + 2 | 0 | CLOSE cursor_stats_1 + 2 | 0 | DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT $1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- FETCH +BEGIN; +DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2; +DECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT 3; +FETCH 1 IN cursor_stats_1; + ?column? +---------- + 2 +(1 row) + +FETCH 1 IN cursor_stats_2; + ?column? +---------- + 3 +(1 row) + +CLOSE cursor_stats_1; +CLOSE cursor_stats_2; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------- + 1 | 0 | BEGIN + 1 | 0 | CLOSE cursor_stats_1 + 1 | 0 | CLOSE cursor_stats_2 + 1 | 0 | COMMIT + 1 | 0 | DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT $1 + 1 | 0 | DECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT $1 + 1 | 1 | FETCH 1 IN cursor_stats_1 + 1 | 1 | FETCH 1 IN cursor_stats_2 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(9 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/dml.out.17 b/edb_stat_statements/expected/dml.out.17 new file mode 100644 index 00000000000..c79f836350e --- /dev/null +++ b/edb_stat_statements/expected/dml.out.17 @@ -0,0 +1,174 @@ +-- +-- DMLs on test table +-- +SET edb_stat_statements.track_utility = FALSE; +CREATE TEMP TABLE pgss_dml_tab (a int, b char(20)); +INSERT INTO pgss_dml_tab VALUES(generate_series(1, 10), 'aaa'); +UPDATE pgss_dml_tab SET b = 'bbb' WHERE a > 7; +DELETE FROM pgss_dml_tab WHERE a > 9; +-- explicit transaction +BEGIN; +UPDATE pgss_dml_tab SET b = '111' WHERE a = 1 ; +COMMIT; +BEGIN \; +UPDATE pgss_dml_tab SET b = '222' WHERE a = 2 \; +COMMIT ; +UPDATE pgss_dml_tab SET b = '333' WHERE a = 3 \; +UPDATE pgss_dml_tab SET b = '444' WHERE a = 4 ; +BEGIN \; +UPDATE pgss_dml_tab SET b = '555' WHERE a = 5 \; +UPDATE pgss_dml_tab SET b = '666' WHERE a = 6 \; +COMMIT ; +-- many INSERT values +INSERT INTO pgss_dml_tab (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c'); +-- SELECT with constants +SELECT * FROM pgss_dml_tab WHERE a > 5 ORDER BY a ; + a | b +---+---------------------- + 6 | 666 + 7 | aaa + 8 | bbb + 9 | bbb +(4 rows) + +SELECT * + FROM pgss_dml_tab + WHERE a > 9 + ORDER BY a ; + a | b +---+--- +(0 rows) + +-- these two need to be done on a different table +-- SELECT without constants +SELECT * FROM pgss_dml_tab ORDER BY a; + a | b +---+---------------------- + 1 | a + 1 | 111 + 2 | b + 2 | 222 + 3 | c + 3 | 333 + 4 | 444 + 5 | 555 + 6 | 666 + 7 | aaa + 8 | bbb + 9 | bbb +(12 rows) + +-- SELECT with IN clause +SELECT * FROM pgss_dml_tab WHERE a IN (1, 2, 3, 4, 5); + a | b +---+---------------------- + 1 | 111 + 2 | 222 + 3 | 333 + 4 | 444 + 5 | 555 + 1 | a + 2 | b + 3 | c +(8 rows) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+--------------------------------------------------------------------- + 1 | 1 | DELETE FROM pgss_dml_tab WHERE a > $1 + 1 | 3 | INSERT INTO pgss_dml_tab (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) + 1 | 10 | INSERT INTO pgss_dml_tab VALUES(generate_series($1, $2), $3) + 1 | 12 | SELECT * FROM pgss_dml_tab ORDER BY a + 2 | 4 | SELECT * FROM pgss_dml_tab WHERE a > $1 ORDER BY a + 1 | 8 | SELECT * FROM pgss_dml_tab WHERE a IN ($1, $2, $3, $4, $5) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | SET edb_stat_statements.track_utility = FALSE + 6 | 6 | UPDATE pgss_dml_tab SET b = $1 WHERE a = $2 + 1 | 3 | UPDATE pgss_dml_tab SET b = $1 WHERE a > $2 +(10 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- MERGE +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED AND length(st.b) > 1 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, NULL); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT VALUES (0, NULL); -- same as above +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (b, a) VALUES (NULL, 0); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (a) VALUES (0); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN DELETE; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN DO NOTHING; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN NOT MATCHED THEN DO NOTHING; +DROP TABLE pgss_dml_tab; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------------------------------------------- + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED AND length(st.b) > $2 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN DELETE + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN DO NOTHING + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN NOT MATCHED THEN DO NOTHING + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + + | | WHEN NOT MATCHED THEN INSERT (a) VALUES ($1) + 2 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + + | | WHEN NOT MATCHED THEN INSERT (a, b) VALUES ($1, $2) + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + + | | WHEN NOT MATCHED THEN INSERT (b, a) VALUES ($1, $2) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(10 rows) + +-- check that [temp] table relation extensions are tracked as writes +CREATE TABLE pgss_extend_tab (a int, b text); +CREATE TEMP TABLE pgss_extend_temp_tab (a int, b text); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +INSERT INTO pgss_extend_tab (a, b) SELECT generate_series(1, 1000), 'something'; +INSERT INTO pgss_extend_temp_tab (a, b) SELECT generate_series(1, 1000), 'something'; +WITH sizes AS ( + SELECT + pg_relation_size('pgss_extend_tab') / current_setting('block_size')::int8 AS rel_size, + pg_relation_size('pgss_extend_temp_tab') / current_setting('block_size')::int8 AS temp_rel_size +) +SELECT + SUM(local_blks_written) >= (SELECT temp_rel_size FROM sizes) AS temp_written_ok, + SUM(local_blks_dirtied) >= (SELECT temp_rel_size FROM sizes) AS temp_dirtied_ok, + SUM(shared_blks_written) >= (SELECT rel_size FROM sizes) AS written_ok, + SUM(shared_blks_dirtied) >= (SELECT rel_size FROM sizes) AS dirtied_ok +FROM edb_stat_statements; + temp_written_ok | temp_dirtied_ok | written_ok | dirtied_ok +-----------------+-----------------+------------+------------ + t | t | t | t +(1 row) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/dml.out.18 b/edb_stat_statements/expected/dml.out.18 new file mode 100644 index 00000000000..68ffd4bd459 --- /dev/null +++ b/edb_stat_statements/expected/dml.out.18 @@ -0,0 +1,174 @@ +-- +-- DMLs on test table +-- +SET edb_stat_statements.track_utility = FALSE; +CREATE TEMP TABLE pgss_dml_tab (a int, b char(20)); +INSERT INTO pgss_dml_tab VALUES(generate_series(1, 10), 'aaa'); +UPDATE pgss_dml_tab SET b = 'bbb' WHERE a > 7; +DELETE FROM pgss_dml_tab WHERE a > 9; +-- explicit transaction +BEGIN; +UPDATE pgss_dml_tab SET b = '111' WHERE a = 1 ; +COMMIT; +BEGIN \; +UPDATE pgss_dml_tab SET b = '222' WHERE a = 2 \; +COMMIT ; +UPDATE pgss_dml_tab SET b = '333' WHERE a = 3 \; +UPDATE pgss_dml_tab SET b = '444' WHERE a = 4 ; +BEGIN \; +UPDATE pgss_dml_tab SET b = '555' WHERE a = 5 \; +UPDATE pgss_dml_tab SET b = '666' WHERE a = 6 \; +COMMIT ; +-- many INSERT values +INSERT INTO pgss_dml_tab (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c'); +-- SELECT with constants +SELECT * FROM pgss_dml_tab WHERE a > 5 ORDER BY a ; + a | b +---+---------------------- + 6 | 666 + 7 | aaa + 8 | bbb + 9 | bbb +(4 rows) + +SELECT * + FROM pgss_dml_tab + WHERE a > 9 + ORDER BY a ; + a | b +---+--- +(0 rows) + +-- these two need to be done on a different table +-- SELECT without constants +SELECT * FROM pgss_dml_tab ORDER BY a; + a | b +---+---------------------- + 1 | a + 1 | 111 + 2 | b + 2 | 222 + 3 | c + 3 | 333 + 4 | 444 + 5 | 555 + 6 | 666 + 7 | aaa + 8 | bbb + 9 | bbb +(12 rows) + +-- SELECT with IN clause +SELECT * FROM pgss_dml_tab WHERE a IN (1, 2, 3, 4, 5); + a | b +---+---------------------- + 1 | 111 + 2 | 222 + 3 | 333 + 4 | 444 + 5 | 555 + 1 | a + 2 | b + 3 | c +(8 rows) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+--------------------------------------------------------------------- + 1 | 1 | DELETE FROM pgss_dml_tab WHERE a > $1 + 1 | 3 | INSERT INTO pgss_dml_tab (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) + 1 | 10 | INSERT INTO pgss_dml_tab VALUES(generate_series($1, $2), $3) + 1 | 12 | SELECT * FROM pgss_dml_tab ORDER BY a + 2 | 4 | SELECT * FROM pgss_dml_tab WHERE a > $1 ORDER BY a + 1 | 8 | SELECT * FROM pgss_dml_tab WHERE a IN ($1, $2, $3, $4, $5) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | SET edb_stat_statements.track_utility = $1 + 6 | 6 | UPDATE pgss_dml_tab SET b = $1 WHERE a = $2 + 1 | 3 | UPDATE pgss_dml_tab SET b = $1 WHERE a > $2 +(10 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- MERGE +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED AND length(st.b) > 1 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, NULL); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT VALUES (0, NULL); -- same as above +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (b, a) VALUES (NULL, 0); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (a) VALUES (0); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN DELETE; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN DO NOTHING; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN NOT MATCHED THEN DO NOTHING; +DROP TABLE pgss_dml_tab; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------------------------------------------- + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED AND length(st.b) > $2 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN DELETE + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN DO NOTHING + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text + 1 | 6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+ + | | WHEN NOT MATCHED THEN DO NOTHING + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + + | | WHEN NOT MATCHED THEN INSERT (a) VALUES ($1) + 2 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + + | | WHEN NOT MATCHED THEN INSERT (a, b) VALUES ($1, $2) + 1 | 0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + + | | WHEN NOT MATCHED THEN INSERT (b, a) VALUES ($1, $2) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(10 rows) + +-- check that [temp] table relation extensions are tracked as writes +CREATE TABLE pgss_extend_tab (a int, b text); +CREATE TEMP TABLE pgss_extend_temp_tab (a int, b text); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +INSERT INTO pgss_extend_tab (a, b) SELECT generate_series(1, 1000), 'something'; +INSERT INTO pgss_extend_temp_tab (a, b) SELECT generate_series(1, 1000), 'something'; +WITH sizes AS ( + SELECT + pg_relation_size('pgss_extend_tab') / current_setting('block_size')::int8 AS rel_size, + pg_relation_size('pgss_extend_temp_tab') / current_setting('block_size')::int8 AS temp_rel_size +) +SELECT + SUM(local_blks_written) >= (SELECT temp_rel_size FROM sizes) AS temp_written_ok, + SUM(local_blks_dirtied) >= (SELECT temp_rel_size FROM sizes) AS temp_dirtied_ok, + SUM(shared_blks_written) >= (SELECT rel_size FROM sizes) AS written_ok, + SUM(shared_blks_dirtied) >= (SELECT rel_size FROM sizes) AS dirtied_ok +FROM edb_stat_statements; + temp_written_ok | temp_dirtied_ok | written_ok | dirtied_ok +-----------------+-----------------+------------+------------ + t | t | t | t +(1 row) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/entry_timestamp.out b/edb_stat_statements/expected/entry_timestamp.out new file mode 100644 index 00000000000..278dede997d --- /dev/null +++ b/edb_stat_statements/expected/entry_timestamp.out @@ -0,0 +1,159 @@ +-- +-- statement timestamps +-- +-- planning time is needed during tests +SET edb_stat_statements.track_planning = TRUE; +SELECT 1 AS "STMTTS1"; + STMTTS1 +--------- + 1 +(1 row) + +SELECT now() AS ref_ts \gset +SELECT 1,2 AS "STMTTS2"; + ?column? | STMTTS2 +----------+--------- + 1 | 2 +(1 row) + +SELECT stats_since >= :'ref_ts', count(*) FROM edb_stat_statements +WHERE query LIKE '%STMTTS%' +GROUP BY stats_since >= :'ref_ts' +ORDER BY stats_since >= :'ref_ts'; + ?column? | count +----------+------- + f | 1 + t | 1 +(2 rows) + +SELECT now() AS ref_ts \gset +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_plan_time + max_plan_time = 0 + ) as minmax_plan_zero, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_stats_since_after_ref, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + total | minmax_plan_zero | minmax_exec_zero | minmax_stats_since_after_ref | stats_since_after_ref +-------+------------------+------------------+------------------------------+----------------------- + 2 | 0 | 0 | 0 | 0 +(1 row) + +-- Perform single min/max reset +SELECT edb_stat_statements_reset(0, '{}', queryid, true) AS minmax_reset_ts +FROM edb_stat_statements +WHERE query LIKE '%STMTTS1%' \gset +-- check +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_plan_time + max_plan_time = 0 + ) as minmax_plan_zero, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_stats_since_after_ref, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + total | minmax_plan_zero | minmax_exec_zero | minmax_stats_since_after_ref | stats_since_after_ref +-------+------------------+------------------+------------------------------+----------------------- + 2 | 1 | 1 | 1 | 0 +(1 row) + +-- check minmax reset timestamps +SELECT +query, minmax_stats_since = :'minmax_reset_ts' AS reset_ts_match +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%' +ORDER BY query COLLATE "C"; + query | reset_ts_match +---------------------------+---------------- + SELECT $1 AS "STMTTS1" | t + SELECT $1,$2 AS "STMTTS2" | f +(2 rows) + +-- check that minmax reset does not set stats_reset +SELECT +stats_reset = :'minmax_reset_ts' AS stats_reset_ts_match +FROM edb_stat_statements_info; + stats_reset_ts_match +---------------------- + f +(1 row) + +-- Perform common min/max reset +SELECT edb_stat_statements_reset(0, '{}', 0, true) AS minmax_reset_ts \gset +-- check again +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_plan_time + max_plan_time = 0 + ) as minmax_plan_zero, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_ts_after_ref, + count(*) FILTER ( + WHERE minmax_stats_since = :'minmax_reset_ts' + ) as minmax_ts_match, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + total | minmax_plan_zero | minmax_exec_zero | minmax_ts_after_ref | minmax_ts_match | stats_since_after_ref +-------+------------------+------------------+---------------------+-----------------+----------------------- + 2 | 2 | 2 | 2 | 2 | 0 +(1 row) + +-- Execute first query once more to check stats update +SELECT 1 AS "STMTTS1"; + STMTTS1 +--------- + 1 +(1 row) + +-- check +-- we don't check planing times here to be independent of +-- plan caching approach +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_ts_after_ref, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + total | minmax_exec_zero | minmax_ts_after_ref | stats_since_after_ref +-------+------------------+---------------------+----------------------- + 2 | 1 | 2 | 0 +(1 row) + +-- Cleanup +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/extended.out b/edb_stat_statements/expected/extended.out new file mode 100644 index 00000000000..febe7c6f5aa --- /dev/null +++ b/edb_stat_statements/expected/extended.out @@ -0,0 +1,70 @@ +-- Tests with extended query protocol +SET edb_stat_statements.track_utility = FALSE; +-- This test checks that an execute message sets a query ID. +SELECT query_id IS NOT NULL AS query_id_set + FROM pg_stat_activity WHERE pid = pg_backend_pid() \bind \g + query_id_set +-------------- + t +(1 row) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT $1 \parse stmt1 +SELECT $1, $2 \parse stmt2 +SELECT $1, $2, $3 \parse stmt3 +SELECT $1 \bind 'unnamed_val1' \g + ?column? +-------------- + unnamed_val1 +(1 row) + +\bind_named stmt1 'stmt1_val1' \g + ?column? +------------ + stmt1_val1 +(1 row) + +\bind_named stmt2 'stmt2_val1' 'stmt2_val2' \g + ?column? | ?column? +------------+------------ + stmt2_val1 | stmt2_val2 +(1 row) + +\bind_named stmt3 'stmt3_val1' 'stmt3_val2' 'stmt3_val3' \g + ?column? | ?column? | ?column? +------------+------------+------------ + stmt3_val1 | stmt3_val2 | stmt3_val3 +(1 row) + +\bind_named stmt3 'stmt3_val4' 'stmt3_val5' 'stmt3_val6' \g + ?column? | ?column? | ?column? +------------+------------+------------ + stmt3_val4 | stmt3_val5 | stmt3_val6 +(1 row) + +\bind_named stmt2 'stmt2_val3' 'stmt2_val4' \g + ?column? | ?column? +------------+------------ + stmt2_val3 | stmt2_val4 +(1 row) + +\bind_named stmt1 'stmt1_val1' \g + ?column? +------------ + stmt1_val1 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 3 | 3 | SELECT $1 + 2 | 2 | SELECT $1, $2 + 2 | 2 | SELECT $1, $2, $3 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(4 rows) + diff --git a/edb_stat_statements/expected/level_tracking.out.17 b/edb_stat_statements/expected/level_tracking.out.17 new file mode 100644 index 00000000000..c5de894cb6d --- /dev/null +++ b/edb_stat_statements/expected/level_tracking.out.17 @@ -0,0 +1,363 @@ +-- +-- Statement level tracking +-- +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- DO block - top-level tracking. +CREATE TABLE stats_track_tab (x int); +SET edb_stat_statements.track = 'top'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; +$$ LANGUAGE plpgsql; +SELECT toplevel, calls, query FROM edb_stat_statements + WHERE query LIKE '%DELETE%' ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+-------------------------------- + t | 1 | DELETE FROM stats_track_tab + t | 1 | DO $$ + + | | BEGIN + + | | DELETE FROM stats_track_tab;+ + | | END; + + | | $$ LANGUAGE plpgsql +(2 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- DO block - all-level tracking. +SET edb_stat_statements.track = 'all'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + f | 1 | DELETE FROM stats_track_tab + t | 1 | DELETE FROM stats_track_tab + t | 1 | DO $$ + + | | BEGIN + + | | DELETE FROM stats_track_tab; + + | | END; $$ + t | 1 | DO LANGUAGE plpgsql $$ + + | | BEGIN + + | | -- this is a SELECT + + | | PERFORM 'hello world'::TEXT; + + | | END; $$ + f | 1 | SELECT $1::TEXT + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + t | 1 | SET edb_stat_statements.track = 'all' +(7 rows) + +-- Procedure with multiple utility statements. +CREATE OR REPLACE PROCEDURE proc_with_utility_stmt() +LANGUAGE SQL +AS $$ + SHOW edb_stat_statements.track; + show edb_stat_statements.track; + SHOW edb_stat_statements.track_utility; +$$; +SET edb_stat_statements.track_utility = TRUE; +-- all-level tracking. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL proc_with_utility_stmt(); +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + t | 1 | CALL proc_with_utility_stmt() + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + f | 2 | SHOW edb_stat_statements.track + f | 1 | SHOW edb_stat_statements.track_utility +(4 rows) + +-- top-level tracking. +SET edb_stat_statements.track = 'top'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL proc_with_utility_stmt(); +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + t | 1 | CALL proc_with_utility_stmt() + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(2 rows) + +-- DO block - top-level tracking without utility. +SET edb_stat_statements.track = 'top'; +SET edb_stat_statements.track_utility = FALSE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + t | 1 | DELETE FROM stats_track_tab + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(2 rows) + +-- DO block - all-level tracking without utility. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + f | 1 | DELETE FROM stats_track_tab + t | 1 | DELETE FROM stats_track_tab + f | 1 | SELECT $1::TEXT + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(4 rows) + +-- PL/pgSQL function - top-level tracking. +SET edb_stat_statements.track = 'top'; +SET edb_stat_statements.track_utility = FALSE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$ +DECLARE + r INTEGER; +BEGIN + SELECT (i + 1 + 1.0)::INTEGER INTO r; + RETURN r; +END; $$ LANGUAGE plpgsql; +SELECT PLUS_TWO(3); + plus_two +---------- + 5 +(1 row) + +SELECT PLUS_TWO(7); + plus_two +---------- + 9 +(1 row) + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS +$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL; +SELECT PLUS_ONE(8); + plus_one +---------- + 9 +(1 row) + +SELECT PLUS_ONE(10); + plus_one +---------- + 11 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 2 | SELECT PLUS_ONE($1) + 2 | 2 | SELECT PLUS_TWO($1) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL; +SELECT PLUS_THREE(8); + plus_three +------------ + 11 +(1 row) + +SELECT PLUS_THREE(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + toplevel | calls | rows | query +----------+-------+------+------------------------------------------------------------------------------- + t | 2 | 2 | SELECT PLUS_ONE($1) + t | 2 | 2 | SELECT PLUS_THREE($1) + t | 2 | 2 | SELECT PLUS_TWO($1) + t | 1 | 3 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C" + t | 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +-- PL/pgSQL function - all-level tracking. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- we drop and recreate the functions to avoid any caching funnies +DROP FUNCTION PLUS_ONE(INTEGER); +DROP FUNCTION PLUS_TWO(INTEGER); +DROP FUNCTION PLUS_THREE(INTEGER); +-- PL/pgSQL function +CREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$ +DECLARE + r INTEGER; +BEGIN + SELECT (i + 1 + 1.0)::INTEGER INTO r; + RETURN r; +END; $$ LANGUAGE plpgsql; +SELECT PLUS_TWO(-1); + plus_two +---------- + 1 +(1 row) + +SELECT PLUS_TWO(2); + plus_two +---------- + 4 +(1 row) + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS +$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL; +SELECT PLUS_ONE(3); + plus_one +---------- + 4 +(1 row) + +SELECT PLUS_ONE(1); + plus_one +---------- + 2 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 2 | SELECT (i + $2 + $3)::INTEGER + 2 | 2 | SELECT (i + $2)::INTEGER LIMIT $3 + 2 | 2 | SELECT PLUS_ONE($1) + 2 | 2 | SELECT PLUS_TWO($1) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL; +SELECT PLUS_THREE(8); + plus_three +------------ + 11 +(1 row) + +SELECT PLUS_THREE(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + toplevel | calls | rows | query +----------+-------+------+------------------------------------------------------------------------------- + f | 2 | 2 | SELECT (i + $2 + $3)::INTEGER + f | 2 | 2 | SELECT (i + $2)::INTEGER LIMIT $3 + t | 2 | 2 | SELECT PLUS_ONE($1) + t | 2 | 2 | SELECT PLUS_THREE($1) + t | 2 | 2 | SELECT PLUS_TWO($1) + t | 1 | 5 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C" + t | 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + f | 2 | 2 | SELECT i + $2 LIMIT $3 +(8 rows) + +-- +-- edb_stat_statements.track = none +-- +SET edb_stat_statements.track = 'none'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT 1 AS "one"; + one +----- + 1 +(1 row) + +SELECT 1 + 1 AS "two"; + two +----- + 2 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------- +(0 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/level_tracking.out.18 b/edb_stat_statements/expected/level_tracking.out.18 new file mode 100644 index 00000000000..876f0c1aa1e --- /dev/null +++ b/edb_stat_statements/expected/level_tracking.out.18 @@ -0,0 +1,363 @@ +-- +-- Statement level tracking +-- +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- DO block - top-level tracking. +CREATE TABLE stats_track_tab (x int); +SET edb_stat_statements.track = 'top'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; +$$ LANGUAGE plpgsql; +SELECT toplevel, calls, query FROM edb_stat_statements + WHERE query LIKE '%DELETE%' ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+-------------------------------- + t | 1 | DELETE FROM stats_track_tab + t | 1 | DO $$ + + | | BEGIN + + | | DELETE FROM stats_track_tab;+ + | | END; + + | | $$ LANGUAGE plpgsql +(2 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- DO block - all-level tracking. +SET edb_stat_statements.track = 'all'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + f | 1 | DELETE FROM stats_track_tab + t | 1 | DELETE FROM stats_track_tab + t | 1 | DO $$ + + | | BEGIN + + | | DELETE FROM stats_track_tab; + + | | END; $$ + t | 1 | DO LANGUAGE plpgsql $$ + + | | BEGIN + + | | -- this is a SELECT + + | | PERFORM 'hello world'::TEXT; + + | | END; $$ + f | 1 | SELECT $1::TEXT + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + t | 1 | SET edb_stat_statements.track = $1 +(7 rows) + +-- Procedure with multiple utility statements. +CREATE OR REPLACE PROCEDURE proc_with_utility_stmt() +LANGUAGE SQL +AS $$ + SHOW edb_stat_statements.track; + show edb_stat_statements.track; + SHOW edb_stat_statements.track_utility; +$$; +SET edb_stat_statements.track_utility = TRUE; +-- all-level tracking. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL proc_with_utility_stmt(); +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + t | 1 | CALL proc_with_utility_stmt() + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + f | 2 | SHOW edb_stat_statements.track + f | 1 | SHOW edb_stat_statements.track_utility +(4 rows) + +-- top-level tracking. +SET edb_stat_statements.track = 'top'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL proc_with_utility_stmt(); +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + t | 1 | CALL proc_with_utility_stmt() + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(2 rows) + +-- DO block - top-level tracking without utility. +SET edb_stat_statements.track = 'top'; +SET edb_stat_statements.track_utility = FALSE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + t | 1 | DELETE FROM stats_track_tab + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(2 rows) + +-- DO block - all-level tracking without utility. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + toplevel | calls | query +----------+-------+----------------------------------------------------- + f | 1 | DELETE FROM stats_track_tab + t | 1 | DELETE FROM stats_track_tab + f | 1 | SELECT $1::TEXT + t | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(4 rows) + +-- PL/pgSQL function - top-level tracking. +SET edb_stat_statements.track = 'top'; +SET edb_stat_statements.track_utility = FALSE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$ +DECLARE + r INTEGER; +BEGIN + SELECT (i + 1 + 1.0)::INTEGER INTO r; + RETURN r; +END; $$ LANGUAGE plpgsql; +SELECT PLUS_TWO(3); + plus_two +---------- + 5 +(1 row) + +SELECT PLUS_TWO(7); + plus_two +---------- + 9 +(1 row) + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS +$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL; +SELECT PLUS_ONE(8); + plus_one +---------- + 9 +(1 row) + +SELECT PLUS_ONE(10); + plus_one +---------- + 11 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 2 | SELECT PLUS_ONE($1) + 2 | 2 | SELECT PLUS_TWO($1) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL; +SELECT PLUS_THREE(8); + plus_three +------------ + 11 +(1 row) + +SELECT PLUS_THREE(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + toplevel | calls | rows | query +----------+-------+------+------------------------------------------------------------------------------- + t | 2 | 2 | SELECT PLUS_ONE($1) + t | 2 | 2 | SELECT PLUS_THREE($1) + t | 2 | 2 | SELECT PLUS_TWO($1) + t | 1 | 3 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C" + t | 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +-- PL/pgSQL function - all-level tracking. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- we drop and recreate the functions to avoid any caching funnies +DROP FUNCTION PLUS_ONE(INTEGER); +DROP FUNCTION PLUS_TWO(INTEGER); +DROP FUNCTION PLUS_THREE(INTEGER); +-- PL/pgSQL function +CREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$ +DECLARE + r INTEGER; +BEGIN + SELECT (i + 1 + 1.0)::INTEGER INTO r; + RETURN r; +END; $$ LANGUAGE plpgsql; +SELECT PLUS_TWO(-1); + plus_two +---------- + 1 +(1 row) + +SELECT PLUS_TWO(2); + plus_two +---------- + 4 +(1 row) + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS +$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL; +SELECT PLUS_ONE(3); + plus_one +---------- + 4 +(1 row) + +SELECT PLUS_ONE(1); + plus_one +---------- + 2 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 2 | SELECT (i + $2 + $3)::INTEGER + 2 | 2 | SELECT (i + $2)::INTEGER LIMIT $3 + 2 | 2 | SELECT PLUS_ONE($1) + 2 | 2 | SELECT PLUS_TWO($1) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL; +SELECT PLUS_THREE(8); + plus_three +------------ + 11 +(1 row) + +SELECT PLUS_THREE(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + toplevel | calls | rows | query +----------+-------+------+------------------------------------------------------------------------------- + f | 2 | 2 | SELECT (i + $2 + $3)::INTEGER + f | 2 | 2 | SELECT (i + $2)::INTEGER LIMIT $3 + t | 2 | 2 | SELECT PLUS_ONE($1) + t | 2 | 2 | SELECT PLUS_THREE($1) + t | 2 | 2 | SELECT PLUS_TWO($1) + t | 1 | 5 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C" + t | 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + f | 2 | 2 | SELECT i + $2 LIMIT $3 +(8 rows) + +-- +-- edb_stat_statements.track = none +-- +SET edb_stat_statements.track = 'none'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT 1 AS "one"; + one +----- + 1 +(1 row) + +SELECT 1 + 1 AS "two"; + two +----- + 2 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------- +(0 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/oldextversions.out b/edb_stat_statements/expected/oldextversions.out new file mode 100644 index 00000000000..6c17cff894a --- /dev/null +++ b/edb_stat_statements/expected/oldextversions.out @@ -0,0 +1,96 @@ +-- test old extension version entry points +CREATE EXTENSION edb_stat_statements WITH VERSION '1.0'; +SELECT pg_get_functiondef('edb_stat_statements_info'::regproc); + pg_get_functiondef +-------------------------------------------------------------------------------------------------------------------------- + CREATE OR REPLACE FUNCTION public.edb_stat_statements_info(OUT dealloc bigint, OUT stats_reset timestamp with time zone)+ + RETURNS record + + LANGUAGE c + + PARALLEL SAFE STRICT + + AS '$libdir/edb_stat_statements', $function$edb_stat_statements_info$function$ + + +(1 row) + +SELECT pg_get_functiondef('edb_stat_statements_reset'::regproc); + pg_get_functiondef +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE OR REPLACE FUNCTION public.edb_stat_statements_reset(userid oid DEFAULT 0, dbids oid[] DEFAULT '{}'::oid[], queryid bigint DEFAULT 0, minmax_only boolean DEFAULT false)+ + RETURNS timestamp with time zone + + LANGUAGE c + + PARALLEL SAFE STRICT + + AS '$libdir/edb_stat_statements', $function$edb_stat_statements_reset$function$ + + +(1 row) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +\d edb_stat_statements + View "public.edb_stat_statements" + Column | Type | Collation | Nullable | Default +----------------------------+--------------------------+-----------+----------+--------- + userid | oid | | | + dbid | oid | | | + toplevel | boolean | | | + queryid | bigint | | | + query | text | | | + extras | jsonb | | | + id | uuid | | | + stmt_type | smallint | | | + plans | bigint | | | + total_plan_time | double precision | | | + min_plan_time | double precision | | | + max_plan_time | double precision | | | + mean_plan_time | double precision | | | + stddev_plan_time | double precision | | | + calls | bigint | | | + total_exec_time | double precision | | | + min_exec_time | double precision | | | + max_exec_time | double precision | | | + mean_exec_time | double precision | | | + stddev_exec_time | double precision | | | + rows | bigint | | | + shared_blks_hit | bigint | | | + shared_blks_read | bigint | | | + shared_blks_dirtied | bigint | | | + shared_blks_written | bigint | | | + local_blks_hit | bigint | | | + local_blks_read | bigint | | | + local_blks_dirtied | bigint | | | + local_blks_written | bigint | | | + temp_blks_read | bigint | | | + temp_blks_written | bigint | | | + shared_blk_read_time | double precision | | | + shared_blk_write_time | double precision | | | + local_blk_read_time | double precision | | | + local_blk_write_time | double precision | | | + temp_blk_read_time | double precision | | | + temp_blk_write_time | double precision | | | + wal_records | bigint | | | + wal_fpi | bigint | | | + wal_bytes | numeric | | | + jit_functions | bigint | | | + jit_generation_time | double precision | | | + jit_inlining_count | bigint | | | + jit_inlining_time | double precision | | | + jit_optimization_count | bigint | | | + jit_optimization_time | double precision | | | + jit_emission_count | bigint | | | + jit_emission_time | double precision | | | + jit_deform_count | bigint | | | + jit_deform_time | double precision | | | + parallel_workers_to_launch | bigint | | | + parallel_workers_launched | bigint | | | + stats_since | timestamp with time zone | | | + minmax_stats_since | timestamp with time zone | | | + +SELECT count(*) > 0 AS has_data FROM edb_stat_statements; + has_data +---------- + t +(1 row) + +DROP EXTENSION edb_stat_statements; diff --git a/edb_stat_statements/expected/parallel.out.17 b/edb_stat_statements/expected/parallel.out.17 new file mode 100644 index 00000000000..1d4643a3982 --- /dev/null +++ b/edb_stat_statements/expected/parallel.out.17 @@ -0,0 +1,34 @@ +-- +-- Tests for parallel statistics +-- +SET edb_stat_statements.track_utility = FALSE; +-- encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +CREATE TABLE pgss_parallel_tab (a int); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT count(*) FROM pgss_parallel_tab; + count +------- + 0 +(1 row) + +SELECT query, + parallel_workers_to_launch > 0 AS has_workers_to_launch, + parallel_workers_launched > 0 AS has_workers_launched + FROM edb_stat_statements + WHERE query ~ 'SELECT count' + ORDER BY query COLLATE "C"; + query | has_workers_to_launch | has_workers_launched +----------------------------------------+-----------------------+---------------------- + SELECT count(*) FROM pgss_parallel_tab | f | f +(1 row) + +DROP TABLE pgss_parallel_tab; diff --git a/edb_stat_statements/expected/parallel.out.18 b/edb_stat_statements/expected/parallel.out.18 new file mode 100644 index 00000000000..aebe94728f1 --- /dev/null +++ b/edb_stat_statements/expected/parallel.out.18 @@ -0,0 +1,34 @@ +-- +-- Tests for parallel statistics +-- +SET edb_stat_statements.track_utility = FALSE; +-- encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +CREATE TABLE pgss_parallel_tab (a int); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT count(*) FROM pgss_parallel_tab; + count +------- + 0 +(1 row) + +SELECT query, + parallel_workers_to_launch > 0 AS has_workers_to_launch, + parallel_workers_launched > 0 AS has_workers_launched + FROM edb_stat_statements + WHERE query ~ 'SELECT count' + ORDER BY query COLLATE "C"; + query | has_workers_to_launch | has_workers_launched +----------------------------------------+-----------------------+---------------------- + SELECT count(*) FROM pgss_parallel_tab | t | t +(1 row) + +DROP TABLE pgss_parallel_tab; diff --git a/edb_stat_statements/expected/planning.out b/edb_stat_statements/expected/planning.out new file mode 100644 index 00000000000..758d6daa387 --- /dev/null +++ b/edb_stat_statements/expected/planning.out @@ -0,0 +1,88 @@ +-- +-- Information related to planning +-- +-- These tests require track_planning to be enabled. +SET edb_stat_statements.track_planning = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- +-- [re]plan counting +-- +CREATE TABLE stats_plan_test (); +PREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test; +EXECUTE prep1; + count +------- + 0 +(1 row) + +EXECUTE prep1; + count +------- + 0 +(1 row) + +EXECUTE prep1; + count +------- + 0 +(1 row) + +ALTER TABLE stats_plan_test ADD COLUMN x int; +EXECUTE prep1; + count +------- + 0 +(1 row) + +SELECT 42; + ?column? +---------- + 42 +(1 row) + +SELECT 42; + ?column? +---------- + 42 +(1 row) + +SELECT 42; + ?column? +---------- + 42 +(1 row) + +SELECT plans, calls, rows, query FROM edb_stat_statements + WHERE query NOT LIKE 'PREPARE%' ORDER BY query COLLATE "C"; + plans | calls | rows | query +-------+-------+------+----------------------------------------------------------- + 0 | 1 | 0 | ALTER TABLE stats_plan_test ADD COLUMN x int + 0 | 1 | 0 | CREATE TABLE stats_plan_test () + 3 | 3 | 3 | SELECT $1 + 0 | 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | 0 | SELECT plans, calls, rows, query FROM edb_stat_statements+ + | | | WHERE query NOT LIKE $1 ORDER BY query COLLATE "C" +(5 rows) + +-- for the prepared statement we expect at least one replan, but cache +-- invalidations could force more +SELECT plans >= 2 AND plans <= calls AS plans_ok, calls, rows, query FROM edb_stat_statements + WHERE query LIKE 'PREPARE%' ORDER BY query COLLATE "C"; + plans_ok | calls | rows | query +----------+-------+------+------------------------------------------------------- + t | 4 | 4 | PREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test +(1 row) + +-- Cleanup +DROP TABLE stats_plan_test; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/privileges.out b/edb_stat_statements/expected/privileges.out new file mode 100644 index 00000000000..deb9b9b5cdf --- /dev/null +++ b/edb_stat_statements/expected/privileges.out @@ -0,0 +1,97 @@ +-- +-- Only superusers and roles with privileges of the pg_read_all_stats role +-- are allowed to see the SQL text and queryid of queries executed by +-- other users. Other users can see the statistics. +-- +SET edb_stat_statements.track_utility = FALSE; +CREATE ROLE regress_stats_superuser SUPERUSER; +CREATE ROLE regress_stats_user1; +CREATE ROLE regress_stats_user2; +GRANT pg_read_all_stats TO regress_stats_user2; +SET ROLE regress_stats_superuser; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT 1 AS "ONE"; + ONE +----- + 1 +(1 row) + +SET ROLE regress_stats_user1; +SELECT 1+1 AS "TWO"; + TWO +----- + 2 +(1 row) + +-- +-- A superuser can read all columns of queries executed by others, +-- including query text and queryid. +-- +SET ROLE regress_stats_superuser; +SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows + FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid + ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows; + rolname | queryid_bool | query | calls | rows +-------------------------+--------------+-----------------------------------------------------+-------+------ + regress_stats_superuser | t | SELECT $1 AS "ONE" | 1 | 1 + regress_stats_superuser | t | SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + regress_stats_user1 | t | SELECT $1+$2 AS "TWO" | 1 | 1 +(3 rows) + +-- +-- regress_stats_user1 has no privileges to read the query text or +-- queryid of queries executed by others but can see statistics +-- like calls and rows. +-- +SET ROLE regress_stats_user1; +SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows + FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid + ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows; + rolname | queryid_bool | query | calls | rows +-------------------------+--------------+--------------------------+-------+------ + regress_stats_superuser | | | 1 | 1 + regress_stats_superuser | | | 1 | 1 + regress_stats_superuser | | | 1 | 3 + regress_stats_user1 | t | SELECT $1+$2 AS "TWO" | 1 | 1 +(4 rows) + +-- +-- regress_stats_user2, with pg_read_all_stats role privileges, can +-- read all columns, including query text and queryid, of queries +-- executed by others. +-- +SET ROLE regress_stats_user2; +SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows + FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid + ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows; + rolname | queryid_bool | query | calls | rows +-------------------------+--------------+---------------------------------------------------------------------------------+-------+------ + regress_stats_superuser | t | SELECT $1 AS "ONE" | 1 | 1 + regress_stats_superuser | t | SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + regress_stats_superuser | t | SELECT r.rolname, ss.queryid <> $1 AS queryid_bool, ss.query, ss.calls, ss.rows+| 1 | 3 + | | FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid +| | + | | ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows | | + regress_stats_user1 | t | SELECT $1+$2 AS "TWO" | 1 | 1 + regress_stats_user1 | t | SELECT r.rolname, ss.queryid <> $1 AS queryid_bool, ss.query, ss.calls, ss.rows+| 1 | 4 + | | FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid +| | + | | ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows | | +(5 rows) + +-- +-- cleanup +-- +RESET ROLE; +DROP ROLE regress_stats_superuser; +DROP ROLE regress_stats_user1; +DROP ROLE regress_stats_user2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/select.out b/edb_stat_statements/expected/select.out new file mode 100644 index 00000000000..fd92de07082 --- /dev/null +++ b/edb_stat_statements/expected/select.out @@ -0,0 +1,414 @@ +-- +-- SELECT statements +-- +CREATE EXTENSION edb_stat_statements; +SET edb_stat_statements.track_utility = FALSE; +SET edb_stat_statements.track_planning = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- +-- simple and compound statements +-- +SELECT 1 AS "int"; + int +----- + 1 +(1 row) + +SELECT 'hello' + -- multiline + AS "text"; + text +------- + hello +(1 row) + +SELECT 'world' AS "text"; + text +------- + world +(1 row) + +-- transaction +BEGIN; +SELECT 1 AS "int"; + int +----- + 1 +(1 row) + +SELECT 'hello' AS "text"; + text +------- + hello +(1 row) + +COMMIT; +-- compound transaction +BEGIN \; +SELECT 2.0 AS "float" \; +SELECT 'world' AS "text" \; +COMMIT; + float +------- + 2.0 +(1 row) + + text +------- + world +(1 row) + +-- compound with empty statements and spurious leading spacing +\;\; SELECT 3 + 3 \;\;\; SELECT ' ' || ' !' \;\; SELECT 1 + 4 \;; + ?column? +---------- + 6 +(1 row) + + ?column? +---------- + ! +(1 row) + + ?column? +---------- + 5 +(1 row) + +-- non ;-terminated statements +SELECT 1 + 1 + 1 AS "add" \gset +SELECT :add + 1 + 1 AS "add" \; +SELECT :add + 1 + 1 AS "add" \gset + add +----- + 5 +(1 row) + +-- set operator +SELECT 1 AS i UNION SELECT 2 ORDER BY i; + i +--- + 1 + 2 +(2 rows) + +-- ? operator +select '{"a":1, "b":2}'::jsonb ? 'b'; + ?column? +---------- + t +(1 row) + +-- cte +WITH t(f) AS ( + VALUES (1.0), (2.0) +) + SELECT f FROM t ORDER BY f; + f +----- + 1.0 + 2.0 +(2 rows) + +-- prepared statement with parameter +PREPARE pgss_test (int) AS SELECT $1, 'test' LIMIT 1; +EXECUTE pgss_test(1); + ?column? | ?column? +----------+---------- + 1 | test +(1 row) + +DEALLOCATE pgss_test; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------------------- + 1 | 1 | PREPARE pgss_test (int) AS SELECT $1, $2 LIMIT $3 + 4 | 4 | SELECT $1 + + | | -- multiline + + | | AS "text" + 2 | 2 | SELECT $1 + $2 + 3 | 3 | SELECT $1 + $2 + $3 AS "add" + 1 | 1 | SELECT $1 AS "float" + 2 | 2 | SELECT $1 AS "int" + 1 | 2 | SELECT $1 AS i UNION SELECT $2 ORDER BY i + 1 | 1 | SELECT $1 || $2 + 0 | 0 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C" + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 2 | WITH t(f) AS ( + + | | VALUES ($1), ($2) + + | | ) + + | | SELECT f FROM t ORDER BY f + 1 | 1 | select $1::jsonb ? $2 +(12 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- +-- queries with locking clauses +-- +CREATE TABLE pgss_a (id integer PRIMARY KEY); +CREATE TABLE pgss_b (id integer PRIMARY KEY, a_id integer REFERENCES pgss_a); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- control query +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id; + id | id | a_id +----+----+------ +(0 rows) + +-- test range tables +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE; + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a; + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b; + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a, pgss_b; -- matches plain "FOR UPDATE" + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b, pgss_a; + id | id | a_id +----+----+------ +(0 rows) + +-- test strengths +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR NO KEY UPDATE; + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR SHARE; + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR KEY SHARE; + id | id | a_id +----+----+------ +(0 rows) + +-- test wait policies +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE NOWAIT; + id | id | a_id +----+----+------ +(0 rows) + +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE SKIP LOCKED; + id | id | a_id +----+----+------ +(0 rows) + +SELECT calls, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | query +-------+------------------------------------------------------------------------------------------ + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR KEY SHARE + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR NO KEY UPDATE + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR SHARE + 2 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE NOWAIT + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b, pgss_a + 1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE SKIP LOCKED + 0 | SELECT calls, query FROM edb_stat_statements ORDER BY query COLLATE "C" + 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(12 rows) + +DROP TABLE pgss_a, pgss_b CASCADE; +-- +-- access to edb_stat_statements_info view +-- +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT dealloc FROM edb_stat_statements_info; + dealloc +--------- + 0 +(1 row) + +-- FROM [ONLY] +CREATE TABLE tbl_inh(id integer); +CREATE TABLE tbl_inh_1() INHERITS (tbl_inh); +INSERT INTO tbl_inh_1 SELECT 1; +SELECT * FROM tbl_inh; + id +---- + 1 +(1 row) + +SELECT * FROM ONLY tbl_inh; + id +---- +(0 rows) + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FROM%tbl_inh%'; + count +------- + 2 +(1 row) + +-- WITH TIES +CREATE TABLE limitoption AS SELECT 0 AS val FROM generate_series(1, 10); +SELECT * +FROM limitoption +WHERE val < 2 +ORDER BY val +FETCH FIRST 2 ROWS WITH TIES; + val +----- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +SELECT * +FROM limitoption +WHERE val < 2 +ORDER BY val +FETCH FIRST 2 ROW ONLY; + val +----- + 0 + 0 +(2 rows) + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FETCH FIRST%'; + count +------- + 2 +(1 row) + +-- GROUP BY [DISTINCT] +SELECT a, b, c +FROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c) +GROUP BY ROLLUP(a, b), rollup(a, c) +ORDER BY a, b, c; + a | b | c +---+---+--- + 1 | 2 | 3 + 1 | 2 | + 1 | 2 | + 1 | | 3 + 1 | | 3 + 1 | | + 1 | | + 1 | | + 4 | | 6 + 4 | | 6 + 4 | | 6 + 4 | | + 4 | | + 4 | | + 4 | | + 4 | | + 7 | 8 | 9 + 7 | 8 | + 7 | 8 | + 7 | | 9 + 7 | | 9 + 7 | | + 7 | | + 7 | | + | | +(25 rows) + +SELECT a, b, c +FROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c) +GROUP BY DISTINCT ROLLUP(a, b), rollup(a, c) +ORDER BY a, b, c; + a | b | c +---+---+--- + 1 | 2 | 3 + 1 | 2 | + 1 | | 3 + 1 | | + 4 | | 6 + 4 | | 6 + 4 | | + 4 | | + 7 | 8 | 9 + 7 | 8 | + 7 | | 9 + 7 | | + | | +(13 rows) + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%GROUP BY%ROLLUP%'; + count +------- + 2 +(1 row) + +-- GROUPING SET agglevelsup +SELECT ( + SELECT ( + SELECT GROUPING(a,b) FROM (VALUES (1)) v2(c) + ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b) +) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f); + grouping +---------- + 0 + 0 + 0 +(3 rows) + +SELECT ( + SELECT ( + SELECT GROUPING(e,f) FROM (VALUES (1)) v2(c) + ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b) +) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f); + grouping +---------- + 3 + 0 + 1 +(3 rows) + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%SELECT GROUPING%'; + count +------- + 2 +(1 row) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/user_activity.out b/edb_stat_statements/expected/user_activity.out new file mode 100644 index 00000000000..8004961034e --- /dev/null +++ b/edb_stat_statements/expected/user_activity.out @@ -0,0 +1,209 @@ +-- +-- Track user activity and reset them +-- +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CREATE ROLE regress_stats_user1; +CREATE ROLE regress_stats_user2; +SET ROLE regress_stats_user1; +SELECT 1 AS "ONE"; + ONE +----- + 1 +(1 row) + +SELECT 1+1 AS "TWO"; + TWO +----- + 2 +(1 row) + +RESET ROLE; +SET ROLE regress_stats_user2; +SELECT 1 AS "ONE"; + ONE +----- + 1 +(1 row) + +SELECT 1+1 AS "TWO"; + TWO +----- + 2 +(1 row) + +RESET ROLE; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows +-----------------------------------------------------+-------+------ + CREATE ROLE regress_stats_user1 | 1 | 0 + CREATE ROLE regress_stats_user2 | 1 | 0 + RESET ROLE | 2 | 0 + SELECT $1 AS "ONE" | 1 | 1 + SELECT $1 AS "ONE" | 1 | 1 + SELECT $1+$2 AS "TWO" | 1 | 1 + SELECT $1+$2 AS "TWO" | 1 | 1 + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + SET ROLE regress_stats_user1 | 1 | 0 + SET ROLE regress_stats_user2 | 1 | 0 +(10 rows) + +-- +-- Don't reset anything if any of the parameter is NULL +-- +SELECT edb_stat_statements_reset(NULL) IS NOT NULL AS t; + t +--- + f +(1 row) + +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows +-------------------------------------------------------------------------------+-------+------ + CREATE ROLE regress_stats_user1 | 1 | 0 + CREATE ROLE regress_stats_user2 | 1 | 0 + RESET ROLE | 2 | 0 + SELECT $1 AS "ONE" | 1 | 1 + SELECT $1 AS "ONE" | 1 | 1 + SELECT $1+$2 AS "TWO" | 1 | 1 + SELECT $1+$2 AS "TWO" | 1 | 1 + SELECT edb_stat_statements_reset($1) IS NOT NULL AS t | 1 | 1 + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C" | 1 | 10 + SET ROLE regress_stats_user1 | 1 | 0 + SET ROLE regress_stats_user2 | 1 | 0 +(12 rows) + +-- +-- remove query ('SELECT $1+$2 AS "TWO"') executed by regress_stats_user2 +-- in the current_database +-- +SELECT edb_stat_statements_reset( + (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = 'regress_stats_user2'), + ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()), + (SELECT s.queryid FROM edb_stat_statements AS s + WHERE s.query = 'SELECT $1+$2 AS "TWO"' LIMIT 1)) + IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows +---------------------------------------------------------------------------------------+-------+------ + CREATE ROLE regress_stats_user1 | 1 | 0 + CREATE ROLE regress_stats_user2 | 1 | 0 + RESET ROLE | 2 | 0 + SELECT $1 AS "ONE" | 1 | 1 + SELECT $1 AS "ONE" | 1 | 1 + SELECT $1+$2 AS "TWO" | 1 | 1 + SELECT edb_stat_statements_reset( +| 1 | 1 + (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = $1), +| | + ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),+| | + (SELECT s.queryid FROM edb_stat_statements AS s +| | + WHERE s.query = $2 LIMIT $3)) +| | + IS NOT NULL AS t | | + SELECT edb_stat_statements_reset($1) IS NOT NULL AS t | 1 | 1 + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C" | 2 | 22 + SET ROLE regress_stats_user1 | 1 | 0 + SET ROLE regress_stats_user2 | 1 | 0 +(12 rows) + +-- +-- remove query ('SELECT $1 AS "ONE"') executed by two users +-- +SELECT edb_stat_statements_reset(0,'{}',s.queryid) IS NOT NULL AS t + FROM edb_stat_statements AS s WHERE s.query = 'SELECT $1 AS "ONE"'; + t +--- + t + t +(2 rows) + +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows +---------------------------------------------------------------------------------------+-------+------ + CREATE ROLE regress_stats_user1 | 1 | 0 + CREATE ROLE regress_stats_user2 | 1 | 0 + RESET ROLE | 2 | 0 + SELECT $1+$2 AS "TWO" | 1 | 1 + SELECT edb_stat_statements_reset( +| 1 | 1 + (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = $1), +| | + ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),+| | + (SELECT s.queryid FROM edb_stat_statements AS s +| | + WHERE s.query = $2 LIMIT $3)) +| | + IS NOT NULL AS t | | + SELECT edb_stat_statements_reset($1) IS NOT NULL AS t | 1 | 1 + SELECT edb_stat_statements_reset($1,$2,s.queryid) IS NOT NULL AS t +| 1 | 2 + FROM edb_stat_statements AS s WHERE s.query = $3 | | + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C" | 3 | 34 + SET ROLE regress_stats_user1 | 1 | 0 + SET ROLE regress_stats_user2 | 1 | 0 +(11 rows) + +-- +-- remove query of a user (regress_stats_user1) +-- +SELECT edb_stat_statements_reset(r.oid) IS NOT NULL AS t + FROM pg_roles AS r WHERE r.rolname = 'regress_stats_user1'; + t +--- + t +(1 row) + +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows +---------------------------------------------------------------------------------------+-------+------ + CREATE ROLE regress_stats_user1 | 1 | 0 + CREATE ROLE regress_stats_user2 | 1 | 0 + RESET ROLE | 2 | 0 + SELECT edb_stat_statements_reset( +| 1 | 1 + (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = $1), +| | + ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),+| | + (SELECT s.queryid FROM edb_stat_statements AS s +| | + WHERE s.query = $2 LIMIT $3)) +| | + IS NOT NULL AS t | | + SELECT edb_stat_statements_reset($1) IS NOT NULL AS t | 1 | 1 + SELECT edb_stat_statements_reset($1,$2,s.queryid) IS NOT NULL AS t +| 1 | 2 + FROM edb_stat_statements AS s WHERE s.query = $3 | | + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 + SELECT edb_stat_statements_reset(r.oid) IS NOT NULL AS t +| 1 | 1 + FROM pg_roles AS r WHERE r.rolname = $1 | | + SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C" | 4 | 45 + SET ROLE regress_stats_user2 | 1 | 0 +(10 rows) + +-- +-- reset all +-- +SELECT edb_stat_statements_reset(0,'{}',0) IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows +-------------------------------------------------------------+-------+------ + SELECT edb_stat_statements_reset(0,'{}',0) IS NOT NULL AS t | 1 | 1 +(1 row) + +-- +-- cleanup +-- +DROP ROLE regress_stats_user1; +DROP ROLE regress_stats_user2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/utility.out.16 b/edb_stat_statements/expected/utility.out.16 new file mode 100644 index 00000000000..bd297986023 --- /dev/null +++ b/edb_stat_statements/expected/utility.out.16 @@ -0,0 +1,756 @@ +-- +-- Utility commands +-- +-- These tests require track_utility to be enabled. +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Tables, indexes, triggers +CREATE TEMP TABLE tab_stats (a int, b char(20)); +CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0; +ALTER TABLE tab_stats ALTER COLUMN b set default 'a'; +ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b; +ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0); +DROP TABLE tab_stats \; +DROP TABLE IF EXISTS tab_stats \; +-- This DROP query uses two different strings, still they count as one entry. +DROP TABLE IF EXISTS tab_stats \; +Drop Table If Exists tab_stats \; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +NOTICE: table "tab_stats" does not exist, skipping +NOTICE: table "tab_stats" does not exist, skipping +NOTICE: table "tab_stats" does not exist, skipping + calls | rows | query +-------+------+-------------------------------------------------------------------------------------- + 1 | 0 | ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0) + 1 | 0 | ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b + 1 | 0 | ALTER TABLE tab_stats ALTER COLUMN b set default 'a' + 1 | 0 | CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0 + 1 | 0 | CREATE TEMP TABLE tab_stats (a int, b char(20)) + 3 | 0 | DROP TABLE IF EXISTS tab_stats + 1 | 0 | DROP TABLE tab_stats + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Partitions +CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a); +CREATE TABLE pt_stats1 (a int, b int); +ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100); +CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200); +CREATE INDEX pt_stats_index ON ONLY pt_stats (a); +CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a); +ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index; +DROP TABLE pt_stats; +-- Views +CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b; +ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2; +DROP VIEW view_stats; +-- Foreign tables +CREATE FOREIGN DATA WRAPPER wrapper_stats; +CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats; +CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats; +ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1; +ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0); +DROP FOREIGN TABLE foreign_stats; +DROP SERVER server_stats; +DROP FOREIGN DATA WRAPPER wrapper_stats; +-- Functions +CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data')) + RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL + SET work_mem = '256kB'; +DROP FUNCTION func_stats; +-- Rules +CREATE TABLE tab_rule_stats (a int, b int); +CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int); +CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD + INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2); +DROP RULE rules_stats ON tab_rule_stats; +DROP TABLE tab_rule_stats, tab_rule_stats_2; +-- Types +CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2)); +DROP TYPE stats_type; +-- Triggers +CREATE TABLE trigger_tab_stats (a int, b int); +CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql + AS $$ BEGIN return OLD; end; $$; +CREATE TRIGGER trigger_tab_stats + AFTER UPDATE ON trigger_tab_stats + FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true) + EXECUTE FUNCTION trigger_func_stats(); +DROP TABLE trigger_tab_stats; +-- Policies +CREATE TABLE tab_policy_stats (a int, b int); +CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5); +DROP TABLE tab_policy_stats; +-- Statistics +CREATE TABLE tab_expr_stats (a int, b int); +CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats; +DROP TABLE tab_expr_stats; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------------------------- + 1 | 0 | ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1 + 1 | 0 | ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0) + 1 | 0 | ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index + 1 | 0 | ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100) + 1 | 0 | ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2 + 1 | 0 | CREATE FOREIGN DATA WRAPPER wrapper_stats + 1 | 0 | CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats + 1 | 0 | CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))+ + | | RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL + + | | SET work_mem = '256kB' + 1 | 0 | CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql + + | | AS $$ BEGIN return OLD; end; $$ + 1 | 0 | CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a) + 1 | 0 | CREATE INDEX pt_stats_index ON ONLY pt_stats (a) + 1 | 0 | CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5) + 1 | 0 | CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD + + | | INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2) + 1 | 0 | CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats + 1 | 0 | CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats + 1 | 0 | CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a) + 1 | 0 | CREATE TABLE pt_stats1 (a int, b int) + 1 | 0 | CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200) + 1 | 0 | CREATE TABLE tab_expr_stats (a int, b int) + 1 | 0 | CREATE TABLE tab_policy_stats (a int, b int) + 1 | 0 | CREATE TABLE tab_rule_stats (a int, b int) + 1 | 0 | CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int) + 1 | 0 | CREATE TABLE trigger_tab_stats (a int, b int) + 1 | 0 | CREATE TRIGGER trigger_tab_stats + + | | AFTER UPDATE ON trigger_tab_stats + + | | FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true) + + | | EXECUTE FUNCTION trigger_func_stats() + 1 | 0 | CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2)) + 1 | 0 | CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b + 1 | 0 | DROP FOREIGN DATA WRAPPER wrapper_stats + 1 | 0 | DROP FOREIGN TABLE foreign_stats + 1 | 0 | DROP FUNCTION func_stats + 1 | 0 | DROP RULE rules_stats ON tab_rule_stats + 1 | 0 | DROP SERVER server_stats + 1 | 0 | DROP TABLE pt_stats + 1 | 0 | DROP TABLE tab_expr_stats + 1 | 0 | DROP TABLE tab_policy_stats + 1 | 0 | DROP TABLE tab_rule_stats, tab_rule_stats_2 + 1 | 0 | DROP TABLE trigger_tab_stats + 1 | 0 | DROP TYPE stats_type + 1 | 0 | DROP VIEW view_stats + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(39 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Transaction statements +BEGIN; +ABORT; +BEGIN; +ROLLBACK; +-- WORK +BEGIN WORK; +COMMIT WORK; +BEGIN WORK; +ABORT WORK; +-- TRANSACTION +BEGIN TRANSACTION; +COMMIT TRANSACTION; +BEGIN TRANSACTION; +ABORT TRANSACTION; +-- More isolation levels +BEGIN TRANSACTION DEFERRABLE; +COMMIT TRANSACTION AND NO CHAIN; +BEGIN ISOLATION LEVEL SERIALIZABLE; +COMMIT; +BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; +COMMIT; +-- List of A_Const nodes, same lists. +BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE; +COMMIT; +BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+--------------------------------------------------------------------- + 4 | 0 | ABORT + 6 | 0 | BEGIN + 2 | 0 | BEGIN ISOLATION LEVEL SERIALIZABLE + 1 | 0 | BEGIN TRANSACTION DEFERRABLE + 1 | 0 | BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE + 1 | 0 | BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE + 7 | 0 | COMMIT WORK + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Two-phase transactions +BEGIN; +PREPARE TRANSACTION 'stat_trans1'; +COMMIT PREPARED 'stat_trans1'; +BEGIN; +PREPARE TRANSACTION 'stat_trans2'; +ROLLBACK PREPARED 'stat_trans2'; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 0 | BEGIN + 1 | 0 | COMMIT PREPARED 'stat_trans1' + 1 | 0 | PREPARE TRANSACTION 'stat_trans1' + 1 | 0 | PREPARE TRANSACTION 'stat_trans2' + 1 | 0 | ROLLBACK PREPARED 'stat_trans2' + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(6 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Savepoints +BEGIN; +SAVEPOINT sp1; +SAVEPOINT sp2; +SAVEPOINT sp3; +SAVEPOINT sp4; +ROLLBACK TO sp4; +ROLLBACK TO SAVEPOINT sp4; +ROLLBACK TRANSACTION TO SAVEPOINT sp3; +RELEASE sp3; +RELEASE SAVEPOINT sp2; +ROLLBACK TO sp1; +RELEASE SAVEPOINT sp1; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 1 | 0 | BEGIN + 1 | 0 | COMMIT + 1 | 0 | RELEASE SAVEPOINT sp1 + 1 | 0 | RELEASE SAVEPOINT sp2 + 1 | 0 | RELEASE sp3 + 1 | 0 | ROLLBACK TO sp1 + 2 | 0 | ROLLBACK TO sp4 + 1 | 0 | ROLLBACK TRANSACTION TO SAVEPOINT sp3 + 1 | 0 | SAVEPOINT sp1 + 1 | 0 | SAVEPOINT sp2 + 1 | 0 | SAVEPOINT sp3 + 1 | 0 | SAVEPOINT sp4 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(13 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- EXPLAIN statements +-- A Query is used, normalized by the query jumbling. +EXPLAIN (costs off) SELECT 1; + QUERY PLAN +------------ + Result +(1 row) + +EXPLAIN (costs off) SELECT 2; + QUERY PLAN +------------ + Result +(1 row) + +EXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 3; + QUERY PLAN +-------------------------------------- + Function Scan on generate_series tab + Filter: (a = 3) +(2 rows) + +EXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 7; + QUERY PLAN +-------------------------------------- + Function Scan on generate_series tab + Filter: (a = 7) +(2 rows) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+--------------------------------------------------------------------------------- + 2 | 0 | EXPLAIN (costs off) SELECT $1 + 2 | 0 | EXPLAIN (costs off) SELECT a FROM generate_series($1,$2) AS tab(a) WHERE a = $3 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +-- CALL +CREATE OR REPLACE PROCEDURE sum_one(i int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + i)::int INTO r; +END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE sum_two(i int, j int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + j)::int INTO r; +END; $$ LANGUAGE plpgsql; +-- Overloaded functions. +CREATE OR REPLACE PROCEDURE overload(i int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + i)::int INTO r; +END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE overload(i text) AS $$ +DECLARE + r text; +BEGIN + SELECT i::text INTO r; +END; $$ LANGUAGE plpgsql; +-- Mix of IN/OUT parameters. +CREATE OR REPLACE PROCEDURE in_out(i int, i2 OUT int, i3 INOUT int) AS $$ +DECLARE + r int; +BEGIN + i2 := i; + i3 := i3 + i; +END; $$ LANGUAGE plpgsql; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL sum_one(3); +CALL sum_one(199); +CALL sum_two(1,1); +CALL sum_two(1,2); +CALL overload(1); +CALL overload('A'); +CALL in_out(1, NULL, 1); + i2 | i3 +----+---- + 1 | 2 +(1 row) + +CALL in_out(2, 1, 2); + i2 | i3 +----+---- + 2 | 4 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 1 | 0 | CALL in_out(1, NULL, 1) + 1 | 0 | CALL in_out(2, 1, 2) + 1 | 0 | CALL overload('A') + 1 | 0 | CALL overload(1) + 1 | 0 | CALL sum_one(199) + 1 | 0 | CALL sum_one(3) + 1 | 0 | CALL sum_two(1,1) + 1 | 0 | CALL sum_two(1,2) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(9 rows) + +-- COPY +CREATE TABLE copy_stats (a int, b int); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Some queries with A_Const nodes. +COPY (SELECT 1) TO STDOUT; +1 +COPY (SELECT 2) TO STDOUT; +2 +COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT; +1 1 +COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT; +2 2 +COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT; +1 2 +2 3 +COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT; +1 4 +2 5 +COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT; +1 4 +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------- + 1 | 1 | COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT + 1 | 1 | COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT + 1 | 1 | COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT + 1 | 1 | COPY (SELECT 1) TO STDOUT + 1 | 1 | COPY (SELECT 2) TO STDOUT + 1 | 2 | COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT + 1 | 2 | COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +DROP TABLE copy_stats; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- CREATE TABLE AS +-- SELECT queries are normalized, creating matching query IDs. +CREATE TABLE ctas_stats_1 AS SELECT 1 AS a; +DROP TABLE ctas_stats_1; +CREATE TABLE ctas_stats_1 AS SELECT 2 AS a; +DROP TABLE ctas_stats_1; +CREATE TABLE ctas_stats_2 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP TABLE ctas_stats_2; +CREATE TABLE ctas_stats_2 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 1; +DROP TABLE ctas_stats_2; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+-------------------------------------------------------------------- + 2 | 2 | CREATE TABLE ctas_stats_1 AS SELECT $1 AS a + 2 | 4 | CREATE TABLE ctas_stats_2 AS + + | | SELECT a AS col1, $1::int AS col2 + + | | FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5 + 2 | 0 | DROP TABLE ctas_stats_1 + 2 | 0 | DROP TABLE ctas_stats_2 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- CREATE MATERIALIZED VIEW +-- SELECT queries are normalized, creating matching query IDs. +CREATE MATERIALIZED VIEW matview_stats_1 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP MATERIALIZED VIEW matview_stats_1; +CREATE MATERIALIZED VIEW matview_stats_1 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3; +DROP MATERIALIZED VIEW matview_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+-------------------------------------------------------------------- + 2 | 2 | CREATE MATERIALIZED VIEW matview_stats_1 AS + + | | SELECT a AS col1, $1::int AS col2 + + | | FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5 + 2 | 0 | DROP MATERIALIZED VIEW matview_stats_1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- CREATE VIEW +CREATE VIEW view_stats_1 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP VIEW view_stats_1; +CREATE VIEW view_stats_1 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3; +DROP VIEW view_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------------------- + 1 | 0 | CREATE VIEW view_stats_1 AS + + | | SELECT a AS col1, 2::int AS col2 + + | | FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2 + 1 | 0 | CREATE VIEW view_stats_1 AS + + | | SELECT a AS col1, 4::int AS col2 + + | | FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3 + 2 | 0 | DROP VIEW view_stats_1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(4 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Domains +CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0); +ALTER DOMAIN domain_stats SET DEFAULT '3'; +ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1); +DROP DOMAIN domain_stats; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+---------------------------------------------------------------------------- + 1 | 0 | ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1) + 1 | 0 | ALTER DOMAIN domain_stats SET DEFAULT '3' + 1 | 0 | CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0) + 1 | 0 | DROP DOMAIN domain_stats + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Execution statements +SELECT 1 as a; + a +--- + 1 +(1 row) + +PREPARE stat_select AS SELECT $1 AS a; +EXECUTE stat_select (1); + a +--- + 1 +(1 row) + +DEALLOCATE stat_select; +PREPARE stat_select AS SELECT $1 AS a; +EXECUTE stat_select (2); + a +--- + 2 +(1 row) + +DEALLOCATE PREPARE stat_select; +DEALLOCATE ALL; +DEALLOCATE PREPARE ALL; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 0 | DEALLOCATE ALL + 2 | 0 | DEALLOCATE stat_select + 2 | 2 | PREPARE stat_select AS SELECT $1 AS a + 1 | 1 | SELECT $1 as a + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- SET statements. +-- These use two different strings, still they count as one entry. +CREATE ROLE regress_stat_set_1; +CREATE ROLE regress_stat_set_2; +SET work_mem = '1MB'; +Set work_mem = '1MB'; +SET work_mem = '2MB'; +SET work_mem = DEFAULT; +SET work_mem TO DEFAULT; +SET work_mem FROM CURRENT; +BEGIN; +SET LOCAL work_mem = '128kB'; +SET LOCAL work_mem = '256kB'; +SET LOCAL work_mem = DEFAULT; +SET LOCAL work_mem TO DEFAULT; +SET LOCAL work_mem FROM CURRENT; +COMMIT; +RESET work_mem; +SET enable_seqscan = off; +SET enable_seqscan = on; +SET SESSION work_mem = '300kB'; +SET SESSION work_mem = '400kB'; +RESET enable_seqscan; +-- SET TRANSACTION ISOLATION +BEGIN; +SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +COMMIT; +-- SET SESSION AUTHORIZATION +SET SESSION SESSION AUTHORIZATION DEFAULT; +SET SESSION AUTHORIZATION 'regress_stat_set_1'; +SET SESSION AUTHORIZATION 'regress_stat_set_2'; +RESET SESSION AUTHORIZATION; +BEGIN; +SET LOCAL SESSION AUTHORIZATION DEFAULT; +SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1'; +SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2'; +RESET SESSION AUTHORIZATION; +COMMIT; +-- SET SESSION CHARACTERISTICS +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY; +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY; +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE; +-- SET XML OPTION +SET XML OPTION DOCUMENT; +SET XML OPTION CONTENT; +-- SET TIME ZONE +SET TIME ZONE 'America/New_York'; +SET TIME ZONE 'Asia/Tokyo'; +SET TIME ZONE DEFAULT; +SET TIME ZONE LOCAL; +SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0'; +RESET TIME ZONE; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------ + 3 | 0 | BEGIN + 3 | 0 | COMMIT + 1 | 0 | CREATE ROLE regress_stat_set_1 + 1 | 0 | CREATE ROLE regress_stat_set_2 + 2 | 0 | RESET SESSION AUTHORIZATION + 1 | 0 | RESET TIME ZONE + 1 | 0 | RESET enable_seqscan + 1 | 0 | RESET work_mem + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1' + 1 | 0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2' + 1 | 0 | SET LOCAL SESSION AUTHORIZATION DEFAULT + 1 | 0 | SET LOCAL work_mem = '128kB' + 1 | 0 | SET LOCAL work_mem = '256kB' + 2 | 0 | SET LOCAL work_mem = DEFAULT + 1 | 0 | SET LOCAL work_mem FROM CURRENT + 1 | 0 | SET SESSION AUTHORIZATION 'regress_stat_set_1' + 1 | 0 | SET SESSION AUTHORIZATION 'regress_stat_set_2' + 1 | 0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY + 1 | 0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY + 1 | 0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE + 1 | 0 | SET SESSION SESSION AUTHORIZATION DEFAULT + 1 | 0 | SET SESSION work_mem = '300kB' + 1 | 0 | SET SESSION work_mem = '400kB' + 1 | 0 | SET TIME ZONE 'America/New_York' + 1 | 0 | SET TIME ZONE 'Asia/Tokyo' + 1 | 0 | SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0' + 2 | 0 | SET TIME ZONE DEFAULT + 1 | 0 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED + 1 | 0 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ + 1 | 0 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE + 1 | 0 | SET XML OPTION CONTENT + 1 | 0 | SET XML OPTION DOCUMENT + 1 | 0 | SET enable_seqscan = off + 1 | 0 | SET enable_seqscan = on + 2 | 0 | SET work_mem = '1MB' + 1 | 0 | SET work_mem = '2MB' + 2 | 0 | SET work_mem = DEFAULT + 1 | 0 | SET work_mem FROM CURRENT +(39 rows) + +DROP ROLE regress_stat_set_1; +DROP ROLE regress_stat_set_2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- +-- Track the total number of rows retrieved or affected by the utility +-- commands of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED VIEW, +-- REFRESH MATERIALIZED VIEW and SELECT INTO +-- +CREATE TABLE pgss_ctas AS SELECT a, 'ctas' b FROM generate_series(1, 10) a; +SELECT generate_series(1, 10) c INTO pgss_select_into; +COPY pgss_ctas (a, b) FROM STDIN; +CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas; +REFRESH MATERIALIZED VIEW pgss_matv; +BEGIN; +DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv; +FETCH NEXT pgss_cursor; + a | b +---+------ + 1 | ctas +(1 row) + +FETCH FORWARD 5 pgss_cursor; + a | b +---+------ + 2 | ctas + 3 | ctas + 4 | ctas + 5 | ctas + 6 | ctas +(5 rows) + +FETCH FORWARD ALL pgss_cursor; + a | b +----+------ + 7 | ctas + 8 | ctas + 9 | ctas + 10 | ctas + 11 | copy + 12 | copy + 13 | copy +(7 rows) + +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------------- + 1 | 0 | BEGIN + 1 | 0 | COMMIT + 1 | 3 | COPY pgss_ctas (a, b) FROM STDIN + 1 | 13 | CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas + 1 | 10 | CREATE TABLE pgss_ctas AS SELECT a, $1 b FROM generate_series($2, $3) a + 1 | 0 | DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv + 1 | 5 | FETCH FORWARD 5 pgss_cursor + 1 | 7 | FETCH FORWARD ALL pgss_cursor + 1 | 1 | FETCH NEXT pgss_cursor + 1 | 13 | REFRESH MATERIALIZED VIEW pgss_matv + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 10 | SELECT generate_series($1, $2) c INTO pgss_select_into +(12 rows) + +DROP MATERIALIZED VIEW pgss_matv; +DROP TABLE pgss_ctas; +DROP TABLE pgss_select_into; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Special cases. Keep these ones at the end to avoid conflicts. +SET SCHEMA 'foo'; +SET SCHEMA 'public'; +RESET ALL; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 1 | 0 | RESET ALL + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | SET SCHEMA 'foo' + 1 | 0 | SET SCHEMA 'public' +(4 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/utility.out.17 b/edb_stat_statements/expected/utility.out.17 new file mode 100644 index 00000000000..fc581c6d82b --- /dev/null +++ b/edb_stat_statements/expected/utility.out.17 @@ -0,0 +1,745 @@ +-- +-- Utility commands +-- +-- These tests require track_utility to be enabled. +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Tables, indexes, triggers +CREATE TEMP TABLE tab_stats (a int, b char(20)); +CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0; +ALTER TABLE tab_stats ALTER COLUMN b set default 'a'; +ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b; +ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0); +DROP TABLE tab_stats \; +DROP TABLE IF EXISTS tab_stats \; +-- This DROP query uses two different strings, still they count as one entry. +DROP TABLE IF EXISTS tab_stats \; +Drop Table If Exists tab_stats \; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +NOTICE: table "tab_stats" does not exist, skipping +NOTICE: table "tab_stats" does not exist, skipping +NOTICE: table "tab_stats" does not exist, skipping + calls | rows | query +-------+------+-------------------------------------------------------------------------------------- + 1 | 0 | ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0) + 1 | 0 | ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b + 1 | 0 | ALTER TABLE tab_stats ALTER COLUMN b set default 'a' + 1 | 0 | CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0 + 1 | 0 | CREATE TEMP TABLE tab_stats (a int, b char(20)) + 3 | 0 | DROP TABLE IF EXISTS tab_stats + 1 | 0 | DROP TABLE tab_stats + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Partitions +CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a); +CREATE TABLE pt_stats1 (a int, b int); +ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100); +CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200); +CREATE INDEX pt_stats_index ON ONLY pt_stats (a); +CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a); +ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index; +DROP TABLE pt_stats; +-- Views +CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b; +ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2; +DROP VIEW view_stats; +-- Foreign tables +CREATE FOREIGN DATA WRAPPER wrapper_stats; +CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats; +CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats; +ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1; +ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0); +DROP FOREIGN TABLE foreign_stats; +DROP SERVER server_stats; +DROP FOREIGN DATA WRAPPER wrapper_stats; +-- Functions +CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data')) + RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL + SET work_mem = '256kB'; +DROP FUNCTION func_stats; +-- Rules +CREATE TABLE tab_rule_stats (a int, b int); +CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int); +CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD + INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2); +DROP RULE rules_stats ON tab_rule_stats; +DROP TABLE tab_rule_stats, tab_rule_stats_2; +-- Types +CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2)); +DROP TYPE stats_type; +-- Triggers +CREATE TABLE trigger_tab_stats (a int, b int); +CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql + AS $$ BEGIN return OLD; end; $$; +CREATE TRIGGER trigger_tab_stats + AFTER UPDATE ON trigger_tab_stats + FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true) + EXECUTE FUNCTION trigger_func_stats(); +DROP TABLE trigger_tab_stats; +-- Policies +CREATE TABLE tab_policy_stats (a int, b int); +CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5); +DROP TABLE tab_policy_stats; +-- Statistics +CREATE TABLE tab_expr_stats (a int, b int); +CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats; +DROP TABLE tab_expr_stats; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------------------------- + 1 | 0 | ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1 + 1 | 0 | ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0) + 1 | 0 | ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index + 1 | 0 | ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100) + 1 | 0 | ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2 + 1 | 0 | CREATE FOREIGN DATA WRAPPER wrapper_stats + 1 | 0 | CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats + 1 | 0 | CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))+ + | | RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL + + | | SET work_mem = '256kB' + 1 | 0 | CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql + + | | AS $$ BEGIN return OLD; end; $$ + 1 | 0 | CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a) + 1 | 0 | CREATE INDEX pt_stats_index ON ONLY pt_stats (a) + 1 | 0 | CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5) + 1 | 0 | CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD + + | | INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2) + 1 | 0 | CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats + 1 | 0 | CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats + 1 | 0 | CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a) + 1 | 0 | CREATE TABLE pt_stats1 (a int, b int) + 1 | 0 | CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200) + 1 | 0 | CREATE TABLE tab_expr_stats (a int, b int) + 1 | 0 | CREATE TABLE tab_policy_stats (a int, b int) + 1 | 0 | CREATE TABLE tab_rule_stats (a int, b int) + 1 | 0 | CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int) + 1 | 0 | CREATE TABLE trigger_tab_stats (a int, b int) + 1 | 0 | CREATE TRIGGER trigger_tab_stats + + | | AFTER UPDATE ON trigger_tab_stats + + | | FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true) + + | | EXECUTE FUNCTION trigger_func_stats() + 1 | 0 | CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2)) + 1 | 0 | CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b + 1 | 0 | DROP FOREIGN DATA WRAPPER wrapper_stats + 1 | 0 | DROP FOREIGN TABLE foreign_stats + 1 | 0 | DROP FUNCTION func_stats + 1 | 0 | DROP RULE rules_stats ON tab_rule_stats + 1 | 0 | DROP SERVER server_stats + 1 | 0 | DROP TABLE pt_stats + 1 | 0 | DROP TABLE tab_expr_stats + 1 | 0 | DROP TABLE tab_policy_stats + 1 | 0 | DROP TABLE tab_rule_stats, tab_rule_stats_2 + 1 | 0 | DROP TABLE trigger_tab_stats + 1 | 0 | DROP TYPE stats_type + 1 | 0 | DROP VIEW view_stats + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(39 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Transaction statements +BEGIN; +ABORT; +BEGIN; +ROLLBACK; +-- WORK +BEGIN WORK; +COMMIT WORK; +BEGIN WORK; +ABORT WORK; +-- TRANSACTION +BEGIN TRANSACTION; +COMMIT TRANSACTION; +BEGIN TRANSACTION; +ABORT TRANSACTION; +-- More isolation levels +BEGIN TRANSACTION DEFERRABLE; +COMMIT TRANSACTION AND NO CHAIN; +BEGIN ISOLATION LEVEL SERIALIZABLE; +COMMIT; +BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; +COMMIT; +-- List of A_Const nodes, same lists. +BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE; +COMMIT; +BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+--------------------------------------------------------------------- + 4 | 0 | ABORT + 6 | 0 | BEGIN + 2 | 0 | BEGIN ISOLATION LEVEL SERIALIZABLE + 1 | 0 | BEGIN TRANSACTION DEFERRABLE + 1 | 0 | BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE + 1 | 0 | BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE + 7 | 0 | COMMIT WORK + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Two-phase transactions +BEGIN; +PREPARE TRANSACTION 'stat_trans1'; +COMMIT PREPARED 'stat_trans1'; +BEGIN; +PREPARE TRANSACTION 'stat_trans2'; +ROLLBACK PREPARED 'stat_trans2'; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 0 | BEGIN + 1 | 0 | COMMIT PREPARED $1 + 2 | 0 | PREPARE TRANSACTION $1 + 1 | 0 | ROLLBACK PREPARED $1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Savepoints +BEGIN; +SAVEPOINT sp1; +SAVEPOINT sp2; +SAVEPOINT sp3; +SAVEPOINT sp4; +ROLLBACK TO sp4; +ROLLBACK TO SAVEPOINT sp4; +ROLLBACK TRANSACTION TO SAVEPOINT sp3; +RELEASE sp3; +RELEASE SAVEPOINT sp2; +ROLLBACK TO sp1; +RELEASE SAVEPOINT sp1; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 1 | 0 | BEGIN + 1 | 0 | COMMIT + 3 | 0 | RELEASE $1 + 4 | 0 | ROLLBACK TO $1 + 4 | 0 | SAVEPOINT $1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(6 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- EXPLAIN statements +-- A Query is used, normalized by the query jumbling. +EXPLAIN (costs off) SELECT 1; + QUERY PLAN +------------ + Result +(1 row) + +EXPLAIN (costs off) SELECT 2; + QUERY PLAN +------------ + Result +(1 row) + +EXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 3; + QUERY PLAN +-------------------------------------- + Function Scan on generate_series tab + Filter: (a = 3) +(2 rows) + +EXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 7; + QUERY PLAN +-------------------------------------- + Function Scan on generate_series tab + Filter: (a = 7) +(2 rows) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+--------------------------------------------------------------------------------- + 2 | 0 | EXPLAIN (costs off) SELECT $1 + 2 | 0 | EXPLAIN (costs off) SELECT a FROM generate_series($1,$2) AS tab(a) WHERE a = $3 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +-- CALL +CREATE OR REPLACE PROCEDURE sum_one(i int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + i)::int INTO r; +END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE sum_two(i int, j int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + j)::int INTO r; +END; $$ LANGUAGE plpgsql; +-- Overloaded functions. +CREATE OR REPLACE PROCEDURE overload(i int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + i)::int INTO r; +END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE overload(i text) AS $$ +DECLARE + r text; +BEGIN + SELECT i::text INTO r; +END; $$ LANGUAGE plpgsql; +-- Mix of IN/OUT parameters. +CREATE OR REPLACE PROCEDURE in_out(i int, i2 OUT int, i3 INOUT int) AS $$ +DECLARE + r int; +BEGIN + i2 := i; + i3 := i3 + i; +END; $$ LANGUAGE plpgsql; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CALL sum_one(3); +CALL sum_one(199); +CALL sum_two(1,1); +CALL sum_two(1,2); +CALL overload(1); +CALL overload('A'); +CALL in_out(1, NULL, 1); + i2 | i3 +----+---- + 1 | 2 +(1 row) + +CALL in_out(2, 1, 2); + i2 | i3 +----+---- + 2 | 4 +(1 row) + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 0 | CALL in_out($1, $2, $3) + 1 | 0 | CALL overload($1) + 1 | 0 | CALL overload($1) + 2 | 0 | CALL sum_one($1) + 2 | 0 | CALL sum_two($1,$2) + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(6 rows) + +-- COPY +CREATE TABLE copy_stats (a int, b int); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Some queries with A_Const nodes. +COPY (SELECT 1) TO STDOUT; +1 +COPY (SELECT 2) TO STDOUT; +2 +COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT; +1 1 +COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT; +2 2 +COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT; +1 2 +2 3 +COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT; +1 4 +2 5 +COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT; +1 4 +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------- + 1 | 1 | COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT + 1 | 1 | COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT + 1 | 1 | COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT + 1 | 1 | COPY (SELECT 1) TO STDOUT + 1 | 1 | COPY (SELECT 2) TO STDOUT + 1 | 2 | COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT + 1 | 2 | COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(8 rows) + +DROP TABLE copy_stats; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- CREATE TABLE AS +-- SELECT queries are normalized, creating matching query IDs. +CREATE TABLE ctas_stats_1 AS SELECT 1 AS a; +DROP TABLE ctas_stats_1; +CREATE TABLE ctas_stats_1 AS SELECT 2 AS a; +DROP TABLE ctas_stats_1; +CREATE TABLE ctas_stats_2 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP TABLE ctas_stats_2; +CREATE TABLE ctas_stats_2 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 1; +DROP TABLE ctas_stats_2; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+-------------------------------------------------------------------- + 2 | 2 | CREATE TABLE ctas_stats_1 AS SELECT $1 AS a + 2 | 4 | CREATE TABLE ctas_stats_2 AS + + | | SELECT a AS col1, $1::int AS col2 + + | | FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5 + 2 | 0 | DROP TABLE ctas_stats_1 + 2 | 0 | DROP TABLE ctas_stats_2 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- CREATE MATERIALIZED VIEW +-- SELECT queries are normalized, creating matching query IDs. +CREATE MATERIALIZED VIEW matview_stats_1 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP MATERIALIZED VIEW matview_stats_1; +CREATE MATERIALIZED VIEW matview_stats_1 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3; +DROP MATERIALIZED VIEW matview_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+-------------------------------------------------------------------- + 2 | 2 | CREATE MATERIALIZED VIEW matview_stats_1 AS + + | | SELECT a AS col1, $1::int AS col2 + + | | FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5 + 2 | 0 | DROP MATERIALIZED VIEW matview_stats_1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(3 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- CREATE VIEW +CREATE VIEW view_stats_1 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP VIEW view_stats_1; +CREATE VIEW view_stats_1 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3; +DROP VIEW view_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------------------- + 1 | 0 | CREATE VIEW view_stats_1 AS + + | | SELECT a AS col1, 2::int AS col2 + + | | FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2 + 1 | 0 | CREATE VIEW view_stats_1 AS + + | | SELECT a AS col1, 4::int AS col2 + + | | FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3 + 2 | 0 | DROP VIEW view_stats_1 + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(4 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Domains +CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0); +ALTER DOMAIN domain_stats SET DEFAULT '3'; +ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1); +DROP DOMAIN domain_stats; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+---------------------------------------------------------------------------- + 1 | 0 | ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1) + 1 | 0 | ALTER DOMAIN domain_stats SET DEFAULT '3' + 1 | 0 | CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0) + 1 | 0 | DROP DOMAIN domain_stats + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Execution statements +SELECT 1 as a; + a +--- + 1 +(1 row) + +PREPARE stat_select AS SELECT $1 AS a; +EXECUTE stat_select (1); + a +--- + 1 +(1 row) + +DEALLOCATE stat_select; +PREPARE stat_select AS SELECT $1 AS a; +EXECUTE stat_select (2); + a +--- + 2 +(1 row) + +DEALLOCATE PREPARE stat_select; +DEALLOCATE ALL; +DEALLOCATE PREPARE ALL; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 2 | 0 | DEALLOCATE $1 + 2 | 0 | DEALLOCATE ALL + 2 | 2 | PREPARE stat_select AS SELECT $1 AS a + 1 | 1 | SELECT $1 as a + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- SET statements. +-- These use two different strings, still they count as one entry. +CREATE ROLE regress_stat_set_1; +CREATE ROLE regress_stat_set_2; +SET work_mem = '1MB'; +Set work_mem = '1MB'; +SET work_mem = '2MB'; +SET work_mem = DEFAULT; +SET work_mem TO DEFAULT; +SET work_mem FROM CURRENT; +BEGIN; +SET LOCAL work_mem = '128kB'; +SET LOCAL work_mem = '256kB'; +SET LOCAL work_mem = DEFAULT; +SET LOCAL work_mem TO DEFAULT; +SET LOCAL work_mem FROM CURRENT; +COMMIT; +RESET work_mem; +SET enable_seqscan = off; +SET enable_seqscan = on; +SET SESSION work_mem = '300kB'; +SET SESSION work_mem = '400kB'; +RESET enable_seqscan; +-- SET TRANSACTION ISOLATION +BEGIN; +SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +COMMIT; +-- SET SESSION AUTHORIZATION +SET SESSION SESSION AUTHORIZATION DEFAULT; +SET SESSION AUTHORIZATION 'regress_stat_set_1'; +SET SESSION AUTHORIZATION 'regress_stat_set_2'; +RESET SESSION AUTHORIZATION; +BEGIN; +SET LOCAL SESSION AUTHORIZATION DEFAULT; +SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1'; +SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2'; +RESET SESSION AUTHORIZATION; +COMMIT; +-- SET SESSION CHARACTERISTICS +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY; +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY; +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE; +-- SET XML OPTION +SET XML OPTION DOCUMENT; +SET XML OPTION CONTENT; +-- SET TIME ZONE +SET TIME ZONE 'America/New_York'; +SET TIME ZONE 'Asia/Tokyo'; +SET TIME ZONE DEFAULT; +SET TIME ZONE LOCAL; +SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0'; +RESET TIME ZONE; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------ + 3 | 0 | BEGIN + 3 | 0 | COMMIT + 1 | 0 | CREATE ROLE regress_stat_set_1 + 1 | 0 | CREATE ROLE regress_stat_set_2 + 2 | 0 | RESET SESSION AUTHORIZATION + 1 | 0 | RESET TIME ZONE + 1 | 0 | RESET enable_seqscan + 1 | 0 | RESET work_mem + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1' + 1 | 0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2' + 1 | 0 | SET LOCAL SESSION AUTHORIZATION DEFAULT + 1 | 0 | SET LOCAL work_mem = '128kB' + 1 | 0 | SET LOCAL work_mem = '256kB' + 2 | 0 | SET LOCAL work_mem = DEFAULT + 1 | 0 | SET LOCAL work_mem FROM CURRENT + 1 | 0 | SET SESSION AUTHORIZATION 'regress_stat_set_1' + 1 | 0 | SET SESSION AUTHORIZATION 'regress_stat_set_2' + 1 | 0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY + 1 | 0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY + 1 | 0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE + 1 | 0 | SET SESSION SESSION AUTHORIZATION DEFAULT + 1 | 0 | SET SESSION work_mem = '300kB' + 1 | 0 | SET SESSION work_mem = '400kB' + 1 | 0 | SET TIME ZONE 'America/New_York' + 1 | 0 | SET TIME ZONE 'Asia/Tokyo' + 1 | 0 | SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0' + 2 | 0 | SET TIME ZONE DEFAULT + 1 | 0 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED + 1 | 0 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ + 1 | 0 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE + 1 | 0 | SET XML OPTION CONTENT + 1 | 0 | SET XML OPTION DOCUMENT + 1 | 0 | SET enable_seqscan = off + 1 | 0 | SET enable_seqscan = on + 2 | 0 | SET work_mem = '1MB' + 1 | 0 | SET work_mem = '2MB' + 2 | 0 | SET work_mem = DEFAULT + 1 | 0 | SET work_mem FROM CURRENT +(39 rows) + +DROP ROLE regress_stat_set_1; +DROP ROLE regress_stat_set_2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- +-- Track the total number of rows retrieved or affected by the utility +-- commands of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED VIEW, +-- REFRESH MATERIALIZED VIEW and SELECT INTO +-- +CREATE TABLE pgss_ctas AS SELECT a, 'ctas' b FROM generate_series(1, 10) a; +SELECT generate_series(1, 10) c INTO pgss_select_into; +COPY pgss_ctas (a, b) FROM STDIN; +CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas; +REFRESH MATERIALIZED VIEW pgss_matv; +BEGIN; +DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv; +FETCH NEXT pgss_cursor; + a | b +---+------ + 1 | ctas +(1 row) + +FETCH FORWARD 5 pgss_cursor; + a | b +---+------ + 2 | ctas + 3 | ctas + 4 | ctas + 5 | ctas + 6 | ctas +(5 rows) + +FETCH FORWARD ALL pgss_cursor; + a | b +----+------ + 7 | ctas + 8 | ctas + 9 | ctas + 10 | ctas + 11 | copy + 12 | copy + 13 | copy +(7 rows) + +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+------------------------------------------------------------------------- + 1 | 0 | BEGIN + 1 | 0 | COMMIT + 1 | 3 | COPY pgss_ctas (a, b) FROM STDIN + 1 | 13 | CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas + 1 | 10 | CREATE TABLE pgss_ctas AS SELECT a, $1 b FROM generate_series($2, $3) a + 1 | 0 | DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv + 1 | 5 | FETCH FORWARD 5 pgss_cursor + 1 | 7 | FETCH FORWARD ALL pgss_cursor + 1 | 1 | FETCH NEXT pgss_cursor + 1 | 13 | REFRESH MATERIALIZED VIEW pgss_matv + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 10 | SELECT generate_series($1, $2) c INTO pgss_select_into +(12 rows) + +DROP MATERIALIZED VIEW pgss_matv; +DROP TABLE pgss_ctas; +DROP TABLE pgss_select_into; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Special cases. Keep these ones at the end to avoid conflicts. +SET SCHEMA 'foo'; +SET SCHEMA 'public'; +RESET ALL; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + calls | rows | query +-------+------+----------------------------------------------------- + 1 | 0 | RESET ALL + 1 | 1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t + 1 | 0 | SET SCHEMA 'foo' + 1 | 0 | SET SCHEMA 'public' +(4 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/wal.out.17 b/edb_stat_statements/expected/wal.out.17 new file mode 100644 index 00000000000..c12598ef707 --- /dev/null +++ b/edb_stat_statements/expected/wal.out.17 @@ -0,0 +1,30 @@ +-- +-- Validate WAL generation metrics +-- +SET edb_stat_statements.track_utility = FALSE; +CREATE TABLE pgss_wal_tab (a int, b char(20)); +INSERT INTO pgss_wal_tab VALUES(generate_series(1, 10), 'aaa'); +UPDATE pgss_wal_tab SET b = 'bbb' WHERE a > 7; +DELETE FROM pgss_wal_tab WHERE a > 9; +DROP TABLE pgss_wal_tab; +-- Check WAL is generated for the above statements +SELECT query, calls, rows, +wal_bytes > 0 as wal_bytes_generated, +wal_records > 0 as wal_records_generated, +wal_records >= rows as wal_records_ge_rows +FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows | wal_bytes_generated | wal_records_generated | wal_records_ge_rows +--------------------------------------------------------------+-------+------+---------------------+-----------------------+--------------------- + DELETE FROM pgss_wal_tab WHERE a > $1 | 1 | 1 | t | t | t + INSERT INTO pgss_wal_tab VALUES(generate_series($1, $2), $3) | 1 | 10 | t | t | t + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 | f | f | f + SET edb_stat_statements.track_utility = FALSE | 1 | 0 | f | f | t + UPDATE pgss_wal_tab SET b = $1 WHERE a > $2 | 1 | 3 | t | t | t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/expected/wal.out.18 b/edb_stat_statements/expected/wal.out.18 new file mode 100644 index 00000000000..b4c255cdf78 --- /dev/null +++ b/edb_stat_statements/expected/wal.out.18 @@ -0,0 +1,30 @@ +-- +-- Validate WAL generation metrics +-- +SET edb_stat_statements.track_utility = FALSE; +CREATE TABLE pgss_wal_tab (a int, b char(20)); +INSERT INTO pgss_wal_tab VALUES(generate_series(1, 10), 'aaa'); +UPDATE pgss_wal_tab SET b = 'bbb' WHERE a > 7; +DELETE FROM pgss_wal_tab WHERE a > 9; +DROP TABLE pgss_wal_tab; +-- Check WAL is generated for the above statements +SELECT query, calls, rows, +wal_bytes > 0 as wal_bytes_generated, +wal_records > 0 as wal_records_generated, +wal_records >= rows as wal_records_ge_rows +FROM edb_stat_statements ORDER BY query COLLATE "C"; + query | calls | rows | wal_bytes_generated | wal_records_generated | wal_records_ge_rows +--------------------------------------------------------------+-------+------+---------------------+-----------------------+--------------------- + DELETE FROM pgss_wal_tab WHERE a > $1 | 1 | 1 | t | t | t + INSERT INTO pgss_wal_tab VALUES(generate_series($1, $2), $3) | 1 | 10 | t | t | t + SELECT edb_stat_statements_reset() IS NOT NULL AS t | 1 | 1 | f | f | f + SET edb_stat_statements.track_utility = $1 | 1 | 0 | f | f | t + UPDATE pgss_wal_tab SET b = $1 WHERE a > $2 | 1 | 3 | t | t | t +(5 rows) + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + diff --git a/edb_stat_statements/sql/cleanup.sql b/edb_stat_statements/sql/cleanup.sql new file mode 100644 index 00000000000..03e40380b87 --- /dev/null +++ b/edb_stat_statements/sql/cleanup.sql @@ -0,0 +1 @@ +DROP EXTENSION edb_stat_statements; diff --git a/edb_stat_statements/sql/cursors.sql b/edb_stat_statements/sql/cursors.sql new file mode 100644 index 00000000000..2c0b637d488 --- /dev/null +++ b/edb_stat_statements/sql/cursors.sql @@ -0,0 +1,30 @@ +-- +-- Cursors +-- + +-- These tests require track_utility to be enabled. +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- DECLARE +-- SELECT is normalized. +DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 1; +CLOSE cursor_stats_1; +DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2; +CLOSE cursor_stats_1; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- FETCH +BEGIN; +DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2; +DECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT 3; +FETCH 1 IN cursor_stats_1; +FETCH 1 IN cursor_stats_2; +CLOSE cursor_stats_1; +CLOSE cursor_stats_2; +COMMIT; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/dml.sql b/edb_stat_statements/sql/dml.sql new file mode 100644 index 00000000000..bf413e0e00b --- /dev/null +++ b/edb_stat_statements/sql/dml.sql @@ -0,0 +1,95 @@ +-- +-- DMLs on test table +-- + +SET edb_stat_statements.track_utility = FALSE; + +CREATE TEMP TABLE pgss_dml_tab (a int, b char(20)); + +INSERT INTO pgss_dml_tab VALUES(generate_series(1, 10), 'aaa'); +UPDATE pgss_dml_tab SET b = 'bbb' WHERE a > 7; +DELETE FROM pgss_dml_tab WHERE a > 9; + +-- explicit transaction +BEGIN; +UPDATE pgss_dml_tab SET b = '111' WHERE a = 1 ; +COMMIT; + +BEGIN \; +UPDATE pgss_dml_tab SET b = '222' WHERE a = 2 \; +COMMIT ; + +UPDATE pgss_dml_tab SET b = '333' WHERE a = 3 \; +UPDATE pgss_dml_tab SET b = '444' WHERE a = 4 ; + +BEGIN \; +UPDATE pgss_dml_tab SET b = '555' WHERE a = 5 \; +UPDATE pgss_dml_tab SET b = '666' WHERE a = 6 \; +COMMIT ; + +-- many INSERT values +INSERT INTO pgss_dml_tab (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c'); + +-- SELECT with constants +SELECT * FROM pgss_dml_tab WHERE a > 5 ORDER BY a ; + +SELECT * + FROM pgss_dml_tab + WHERE a > 9 + ORDER BY a ; + +-- these two need to be done on a different table +-- SELECT without constants +SELECT * FROM pgss_dml_tab ORDER BY a; + +-- SELECT with IN clause +SELECT * FROM pgss_dml_tab WHERE a IN (1, 2, 3, 4, 5); + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- MERGE +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED AND length(st.b) > 1 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, NULL); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT VALUES (0, NULL); -- same as above +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (b, a) VALUES (NULL, 0); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a) + WHEN NOT MATCHED THEN INSERT (a) VALUES (0); +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN DELETE; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN MATCHED THEN DO NOTHING; +MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4) + WHEN NOT MATCHED THEN DO NOTHING; + +DROP TABLE pgss_dml_tab; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- check that [temp] table relation extensions are tracked as writes +CREATE TABLE pgss_extend_tab (a int, b text); +CREATE TEMP TABLE pgss_extend_temp_tab (a int, b text); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +INSERT INTO pgss_extend_tab (a, b) SELECT generate_series(1, 1000), 'something'; +INSERT INTO pgss_extend_temp_tab (a, b) SELECT generate_series(1, 1000), 'something'; +WITH sizes AS ( + SELECT + pg_relation_size('pgss_extend_tab') / current_setting('block_size')::int8 AS rel_size, + pg_relation_size('pgss_extend_temp_tab') / current_setting('block_size')::int8 AS temp_rel_size +) +SELECT + SUM(local_blks_written) >= (SELECT temp_rel_size FROM sizes) AS temp_written_ok, + SUM(local_blks_dirtied) >= (SELECT temp_rel_size FROM sizes) AS temp_dirtied_ok, + SUM(shared_blks_written) >= (SELECT rel_size FROM sizes) AS written_ok, + SUM(shared_blks_dirtied) >= (SELECT rel_size FROM sizes) AS dirtied_ok +FROM edb_stat_statements; + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/entry_timestamp.sql b/edb_stat_statements/sql/entry_timestamp.sql new file mode 100644 index 00000000000..2e2b096f413 --- /dev/null +++ b/edb_stat_statements/sql/entry_timestamp.sql @@ -0,0 +1,114 @@ +-- +-- statement timestamps +-- + +-- planning time is needed during tests +SET edb_stat_statements.track_planning = TRUE; + +SELECT 1 AS "STMTTS1"; +SELECT now() AS ref_ts \gset +SELECT 1,2 AS "STMTTS2"; +SELECT stats_since >= :'ref_ts', count(*) FROM edb_stat_statements +WHERE query LIKE '%STMTTS%' +GROUP BY stats_since >= :'ref_ts' +ORDER BY stats_since >= :'ref_ts'; + +SELECT now() AS ref_ts \gset +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_plan_time + max_plan_time = 0 + ) as minmax_plan_zero, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_stats_since_after_ref, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + +-- Perform single min/max reset +SELECT edb_stat_statements_reset(0, '{}', queryid, true) AS minmax_reset_ts +FROM edb_stat_statements +WHERE query LIKE '%STMTTS1%' \gset + +-- check +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_plan_time + max_plan_time = 0 + ) as minmax_plan_zero, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_stats_since_after_ref, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + +-- check minmax reset timestamps +SELECT +query, minmax_stats_since = :'minmax_reset_ts' AS reset_ts_match +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%' +ORDER BY query COLLATE "C"; + +-- check that minmax reset does not set stats_reset +SELECT +stats_reset = :'minmax_reset_ts' AS stats_reset_ts_match +FROM edb_stat_statements_info; + +-- Perform common min/max reset +SELECT edb_stat_statements_reset(0, '{}', 0, true) AS minmax_reset_ts \gset + +-- check again +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_plan_time + max_plan_time = 0 + ) as minmax_plan_zero, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_ts_after_ref, + count(*) FILTER ( + WHERE minmax_stats_since = :'minmax_reset_ts' + ) as minmax_ts_match, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + +-- Execute first query once more to check stats update +SELECT 1 AS "STMTTS1"; + +-- check +-- we don't check planing times here to be independent of +-- plan caching approach +SELECT + count(*) as total, + count(*) FILTER ( + WHERE min_exec_time + max_exec_time = 0 + ) as minmax_exec_zero, + count(*) FILTER ( + WHERE minmax_stats_since >= :'ref_ts' + ) as minmax_ts_after_ref, + count(*) FILTER ( + WHERE stats_since >= :'ref_ts' + ) as stats_since_after_ref +FROM edb_stat_statements +WHERE query LIKE '%STMTTS%'; + +-- Cleanup +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/extended.sql b/edb_stat_statements/sql/extended.sql new file mode 100644 index 00000000000..83ca79fa7d5 --- /dev/null +++ b/edb_stat_statements/sql/extended.sql @@ -0,0 +1,21 @@ +-- Tests with extended query protocol + +SET edb_stat_statements.track_utility = FALSE; + +-- This test checks that an execute message sets a query ID. +SELECT query_id IS NOT NULL AS query_id_set + FROM pg_stat_activity WHERE pid = pg_backend_pid() \bind \g + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +SELECT $1 \parse stmt1 +SELECT $1, $2 \parse stmt2 +SELECT $1, $2, $3 \parse stmt3 +SELECT $1 \bind 'unnamed_val1' \g +\bind_named stmt1 'stmt1_val1' \g +\bind_named stmt2 'stmt2_val1' 'stmt2_val2' \g +\bind_named stmt3 'stmt3_val1' 'stmt3_val2' 'stmt3_val3' \g +\bind_named stmt3 'stmt3_val4' 'stmt3_val5' 'stmt3_val6' \g +\bind_named stmt2 'stmt2_val3' 'stmt2_val4' \g +\bind_named stmt1 'stmt1_val1' \g + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; diff --git a/edb_stat_statements/sql/level_tracking.sql b/edb_stat_statements/sql/level_tracking.sql new file mode 100644 index 00000000000..9cb852e8e20 --- /dev/null +++ b/edb_stat_statements/sql/level_tracking.sql @@ -0,0 +1,173 @@ +-- +-- Statement level tracking +-- + +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- DO block - top-level tracking. +CREATE TABLE stats_track_tab (x int); +SET edb_stat_statements.track = 'top'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; +$$ LANGUAGE plpgsql; +SELECT toplevel, calls, query FROM edb_stat_statements + WHERE query LIKE '%DELETE%' ORDER BY query COLLATE "C", toplevel; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- DO block - all-level tracking. +SET edb_stat_statements.track = 'all'; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + +-- Procedure with multiple utility statements. +CREATE OR REPLACE PROCEDURE proc_with_utility_stmt() +LANGUAGE SQL +AS $$ + SHOW edb_stat_statements.track; + show edb_stat_statements.track; + SHOW edb_stat_statements.track_utility; +$$; +SET edb_stat_statements.track_utility = TRUE; +-- all-level tracking. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +CALL proc_with_utility_stmt(); +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; +-- top-level tracking. +SET edb_stat_statements.track = 'top'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +CALL proc_with_utility_stmt(); +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + +-- DO block - top-level tracking without utility. +SET edb_stat_statements.track = 'top'; +SET edb_stat_statements.track_utility = FALSE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + +-- DO block - all-level tracking without utility. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +DELETE FROM stats_track_tab; +DO $$ +BEGIN + DELETE FROM stats_track_tab; +END; $$; +DO LANGUAGE plpgsql $$ +BEGIN + -- this is a SELECT + PERFORM 'hello world'::TEXT; +END; $$; +SELECT toplevel, calls, query FROM edb_stat_statements + ORDER BY query COLLATE "C", toplevel; + +-- PL/pgSQL function - top-level tracking. +SET edb_stat_statements.track = 'top'; +SET edb_stat_statements.track_utility = FALSE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +CREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$ +DECLARE + r INTEGER; +BEGIN + SELECT (i + 1 + 1.0)::INTEGER INTO r; + RETURN r; +END; $$ LANGUAGE plpgsql; + +SELECT PLUS_TWO(3); +SELECT PLUS_TWO(7); + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS +$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL; + +SELECT PLUS_ONE(8); +SELECT PLUS_ONE(10); + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL; + +SELECT PLUS_THREE(8); +SELECT PLUS_THREE(10); + +SELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- PL/pgSQL function - all-level tracking. +SET edb_stat_statements.track = 'all'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- we drop and recreate the functions to avoid any caching funnies +DROP FUNCTION PLUS_ONE(INTEGER); +DROP FUNCTION PLUS_TWO(INTEGER); +DROP FUNCTION PLUS_THREE(INTEGER); + +-- PL/pgSQL function +CREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$ +DECLARE + r INTEGER; +BEGIN + SELECT (i + 1 + 1.0)::INTEGER INTO r; + RETURN r; +END; $$ LANGUAGE plpgsql; + +SELECT PLUS_TWO(-1); +SELECT PLUS_TWO(2); + +-- SQL function --- use LIMIT to keep it from being inlined +CREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS +$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL; + +SELECT PLUS_ONE(3); +SELECT PLUS_ONE(1); + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- immutable SQL function --- can be executed at plan time +CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS +$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL; + +SELECT PLUS_THREE(8); +SELECT PLUS_THREE(10); + +SELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- edb_stat_statements.track = none +-- +SET edb_stat_statements.track = 'none'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +SELECT 1 AS "one"; +SELECT 1 + 1 AS "two"; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/oldextversions.sql b/edb_stat_statements/sql/oldextversions.sql new file mode 100644 index 00000000000..078101ab8ee --- /dev/null +++ b/edb_stat_statements/sql/oldextversions.sql @@ -0,0 +1,13 @@ +-- test old extension version entry points + +CREATE EXTENSION edb_stat_statements WITH VERSION '1.0'; + +SELECT pg_get_functiondef('edb_stat_statements_info'::regproc); + +SELECT pg_get_functiondef('edb_stat_statements_reset'::regproc); + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +\d edb_stat_statements +SELECT count(*) > 0 AS has_data FROM edb_stat_statements; + +DROP EXTENSION edb_stat_statements; diff --git a/edb_stat_statements/sql/parallel.sql b/edb_stat_statements/sql/parallel.sql new file mode 100644 index 00000000000..f4592e147ba --- /dev/null +++ b/edb_stat_statements/sql/parallel.sql @@ -0,0 +1,26 @@ +-- +-- Tests for parallel statistics +-- + +SET edb_stat_statements.track_utility = FALSE; + +-- encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +CREATE TABLE pgss_parallel_tab (a int); + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +SELECT count(*) FROM pgss_parallel_tab; + +SELECT query, + parallel_workers_to_launch > 0 AS has_workers_to_launch, + parallel_workers_launched > 0 AS has_workers_launched + FROM edb_stat_statements + WHERE query ~ 'SELECT count' + ORDER BY query COLLATE "C"; + +DROP TABLE pgss_parallel_tab; diff --git a/edb_stat_statements/sql/planning.sql b/edb_stat_statements/sql/planning.sql new file mode 100644 index 00000000000..618baadb18d --- /dev/null +++ b/edb_stat_statements/sql/planning.sql @@ -0,0 +1,31 @@ +-- +-- Information related to planning +-- + +-- These tests require track_planning to be enabled. +SET edb_stat_statements.track_planning = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- +-- [re]plan counting +-- +CREATE TABLE stats_plan_test (); +PREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test; +EXECUTE prep1; +EXECUTE prep1; +EXECUTE prep1; +ALTER TABLE stats_plan_test ADD COLUMN x int; +EXECUTE prep1; +SELECT 42; +SELECT 42; +SELECT 42; +SELECT plans, calls, rows, query FROM edb_stat_statements + WHERE query NOT LIKE 'PREPARE%' ORDER BY query COLLATE "C"; +-- for the prepared statement we expect at least one replan, but cache +-- invalidations could force more +SELECT plans >= 2 AND plans <= calls AS plans_ok, calls, rows, query FROM edb_stat_statements + WHERE query LIKE 'PREPARE%' ORDER BY query COLLATE "C"; + +-- Cleanup +DROP TABLE stats_plan_test; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/privileges.sql b/edb_stat_statements/sql/privileges.sql new file mode 100644 index 00000000000..1b5459e3067 --- /dev/null +++ b/edb_stat_statements/sql/privileges.sql @@ -0,0 +1,60 @@ +-- +-- Only superusers and roles with privileges of the pg_read_all_stats role +-- are allowed to see the SQL text and queryid of queries executed by +-- other users. Other users can see the statistics. +-- + +SET edb_stat_statements.track_utility = FALSE; +CREATE ROLE regress_stats_superuser SUPERUSER; +CREATE ROLE regress_stats_user1; +CREATE ROLE regress_stats_user2; +GRANT pg_read_all_stats TO regress_stats_user2; + +SET ROLE regress_stats_superuser; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +SELECT 1 AS "ONE"; + +SET ROLE regress_stats_user1; +SELECT 1+1 AS "TWO"; + +-- +-- A superuser can read all columns of queries executed by others, +-- including query text and queryid. +-- + +SET ROLE regress_stats_superuser; +SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows + FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid + ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows; + +-- +-- regress_stats_user1 has no privileges to read the query text or +-- queryid of queries executed by others but can see statistics +-- like calls and rows. +-- + +SET ROLE regress_stats_user1; +SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows + FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid + ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows; + +-- +-- regress_stats_user2, with pg_read_all_stats role privileges, can +-- read all columns, including query text and queryid, of queries +-- executed by others. +-- + +SET ROLE regress_stats_user2; +SELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows + FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid + ORDER BY r.rolname, ss.query COLLATE "C", ss.calls, ss.rows; + +-- +-- cleanup +-- + +RESET ROLE; +DROP ROLE regress_stats_superuser; +DROP ROLE regress_stats_user1; +DROP ROLE regress_stats_user2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/select.sql b/edb_stat_statements/sql/select.sql new file mode 100644 index 00000000000..6847a198161 --- /dev/null +++ b/edb_stat_statements/sql/select.sql @@ -0,0 +1,149 @@ +-- +-- SELECT statements +-- + +CREATE EXTENSION edb_stat_statements; +SET edb_stat_statements.track_utility = FALSE; +SET edb_stat_statements.track_planning = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- +-- simple and compound statements +-- +SELECT 1 AS "int"; + +SELECT 'hello' + -- multiline + AS "text"; + +SELECT 'world' AS "text"; + +-- transaction +BEGIN; +SELECT 1 AS "int"; +SELECT 'hello' AS "text"; +COMMIT; + +-- compound transaction +BEGIN \; +SELECT 2.0 AS "float" \; +SELECT 'world' AS "text" \; +COMMIT; + +-- compound with empty statements and spurious leading spacing +\;\; SELECT 3 + 3 \;\;\; SELECT ' ' || ' !' \;\; SELECT 1 + 4 \;; + +-- non ;-terminated statements +SELECT 1 + 1 + 1 AS "add" \gset +SELECT :add + 1 + 1 AS "add" \; +SELECT :add + 1 + 1 AS "add" \gset + +-- set operator +SELECT 1 AS i UNION SELECT 2 ORDER BY i; + +-- ? operator +select '{"a":1, "b":2}'::jsonb ? 'b'; + +-- cte +WITH t(f) AS ( + VALUES (1.0), (2.0) +) + SELECT f FROM t ORDER BY f; + +-- prepared statement with parameter +PREPARE pgss_test (int) AS SELECT $1, 'test' LIMIT 1; +EXECUTE pgss_test(1); +DEALLOCATE pgss_test; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- +-- queries with locking clauses +-- +CREATE TABLE pgss_a (id integer PRIMARY KEY); +CREATE TABLE pgss_b (id integer PRIMARY KEY, a_id integer REFERENCES pgss_a); + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- control query +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id; + +-- test range tables +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE; +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a; +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b; +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a, pgss_b; -- matches plain "FOR UPDATE" +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b, pgss_a; + +-- test strengths +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR NO KEY UPDATE; +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR SHARE; +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR KEY SHARE; + +-- test wait policies +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE NOWAIT; +SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE SKIP LOCKED; + +SELECT calls, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +DROP TABLE pgss_a, pgss_b CASCADE; + +-- +-- access to edb_stat_statements_info view +-- +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +SELECT dealloc FROM edb_stat_statements_info; + +-- FROM [ONLY] +CREATE TABLE tbl_inh(id integer); +CREATE TABLE tbl_inh_1() INHERITS (tbl_inh); +INSERT INTO tbl_inh_1 SELECT 1; + +SELECT * FROM tbl_inh; +SELECT * FROM ONLY tbl_inh; + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FROM%tbl_inh%'; + +-- WITH TIES +CREATE TABLE limitoption AS SELECT 0 AS val FROM generate_series(1, 10); +SELECT * +FROM limitoption +WHERE val < 2 +ORDER BY val +FETCH FIRST 2 ROWS WITH TIES; + +SELECT * +FROM limitoption +WHERE val < 2 +ORDER BY val +FETCH FIRST 2 ROW ONLY; + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FETCH FIRST%'; + +-- GROUP BY [DISTINCT] +SELECT a, b, c +FROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c) +GROUP BY ROLLUP(a, b), rollup(a, c) +ORDER BY a, b, c; +SELECT a, b, c +FROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c) +GROUP BY DISTINCT ROLLUP(a, b), rollup(a, c) +ORDER BY a, b, c; + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%GROUP BY%ROLLUP%'; + +-- GROUPING SET agglevelsup +SELECT ( + SELECT ( + SELECT GROUPING(a,b) FROM (VALUES (1)) v2(c) + ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b) +) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f); +SELECT ( + SELECT ( + SELECT GROUPING(e,f) FROM (VALUES (1)) v2(c) + ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b) +) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f); + +SELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%SELECT GROUPING%'; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/user_activity.sql b/edb_stat_statements/sql/user_activity.sql new file mode 100644 index 00000000000..47c0e0639fa --- /dev/null +++ b/edb_stat_statements/sql/user_activity.sql @@ -0,0 +1,67 @@ +-- +-- Track user activity and reset them +-- + +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +CREATE ROLE regress_stats_user1; +CREATE ROLE regress_stats_user2; + +SET ROLE regress_stats_user1; + +SELECT 1 AS "ONE"; +SELECT 1+1 AS "TWO"; + +RESET ROLE; +SET ROLE regress_stats_user2; + +SELECT 1 AS "ONE"; +SELECT 1+1 AS "TWO"; + +RESET ROLE; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- Don't reset anything if any of the parameter is NULL +-- +SELECT edb_stat_statements_reset(NULL) IS NOT NULL AS t; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- remove query ('SELECT $1+$2 AS "TWO"') executed by regress_stats_user2 +-- in the current_database +-- +SELECT edb_stat_statements_reset( + (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = 'regress_stats_user2'), + ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()), + (SELECT s.queryid FROM edb_stat_statements AS s + WHERE s.query = 'SELECT $1+$2 AS "TWO"' LIMIT 1)) + IS NOT NULL AS t; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- remove query ('SELECT $1 AS "ONE"') executed by two users +-- +SELECT edb_stat_statements_reset(0,'{}',s.queryid) IS NOT NULL AS t + FROM edb_stat_statements AS s WHERE s.query = 'SELECT $1 AS "ONE"'; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- remove query of a user (regress_stats_user1) +-- +SELECT edb_stat_statements_reset(r.oid) IS NOT NULL AS t + FROM pg_roles AS r WHERE r.rolname = 'regress_stats_user1'; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- reset all +-- +SELECT edb_stat_statements_reset(0,'{}',0) IS NOT NULL AS t; +SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- +-- cleanup +-- +DROP ROLE regress_stats_user1; +DROP ROLE regress_stats_user2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/utility.sql b/edb_stat_statements/sql/utility.sql new file mode 100644 index 00000000000..1d1c3961491 --- /dev/null +++ b/edb_stat_statements/sql/utility.sql @@ -0,0 +1,374 @@ +-- +-- Utility commands +-- + +-- These tests require track_utility to be enabled. +SET edb_stat_statements.track_utility = TRUE; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Tables, indexes, triggers +CREATE TEMP TABLE tab_stats (a int, b char(20)); +CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0; +ALTER TABLE tab_stats ALTER COLUMN b set default 'a'; +ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b; +ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0); +DROP TABLE tab_stats \; +DROP TABLE IF EXISTS tab_stats \; +-- This DROP query uses two different strings, still they count as one entry. +DROP TABLE IF EXISTS tab_stats \; +Drop Table If Exists tab_stats \; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Partitions +CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a); +CREATE TABLE pt_stats1 (a int, b int); +ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100); +CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200); +CREATE INDEX pt_stats_index ON ONLY pt_stats (a); +CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a); +ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index; +DROP TABLE pt_stats; + +-- Views +CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b; +ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2; +DROP VIEW view_stats; + +-- Foreign tables +CREATE FOREIGN DATA WRAPPER wrapper_stats; +CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats; +CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats; +ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1; +ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0); +DROP FOREIGN TABLE foreign_stats; +DROP SERVER server_stats; +DROP FOREIGN DATA WRAPPER wrapper_stats; + +-- Functions +CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data')) + RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL + SET work_mem = '256kB'; +DROP FUNCTION func_stats; + +-- Rules +CREATE TABLE tab_rule_stats (a int, b int); +CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int); +CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD + INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2); +DROP RULE rules_stats ON tab_rule_stats; +DROP TABLE tab_rule_stats, tab_rule_stats_2; + +-- Types +CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2)); +DROP TYPE stats_type; + +-- Triggers +CREATE TABLE trigger_tab_stats (a int, b int); +CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql + AS $$ BEGIN return OLD; end; $$; +CREATE TRIGGER trigger_tab_stats + AFTER UPDATE ON trigger_tab_stats + FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true) + EXECUTE FUNCTION trigger_func_stats(); +DROP TABLE trigger_tab_stats; + +-- Policies +CREATE TABLE tab_policy_stats (a int, b int); +CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5); +DROP TABLE tab_policy_stats; + +-- Statistics +CREATE TABLE tab_expr_stats (a int, b int); +CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats; +DROP TABLE tab_expr_stats; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Transaction statements +BEGIN; +ABORT; +BEGIN; +ROLLBACK; +-- WORK +BEGIN WORK; +COMMIT WORK; +BEGIN WORK; +ABORT WORK; +-- TRANSACTION +BEGIN TRANSACTION; +COMMIT TRANSACTION; +BEGIN TRANSACTION; +ABORT TRANSACTION; +-- More isolation levels +BEGIN TRANSACTION DEFERRABLE; +COMMIT TRANSACTION AND NO CHAIN; +BEGIN ISOLATION LEVEL SERIALIZABLE; +COMMIT; +BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; +COMMIT; +-- List of A_Const nodes, same lists. +BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE; +COMMIT; +BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Two-phase transactions +BEGIN; +PREPARE TRANSACTION 'stat_trans1'; +COMMIT PREPARED 'stat_trans1'; +BEGIN; +PREPARE TRANSACTION 'stat_trans2'; +ROLLBACK PREPARED 'stat_trans2'; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Savepoints +BEGIN; +SAVEPOINT sp1; +SAVEPOINT sp2; +SAVEPOINT sp3; +SAVEPOINT sp4; +ROLLBACK TO sp4; +ROLLBACK TO SAVEPOINT sp4; +ROLLBACK TRANSACTION TO SAVEPOINT sp3; +RELEASE sp3; +RELEASE SAVEPOINT sp2; +ROLLBACK TO sp1; +RELEASE SAVEPOINT sp1; +COMMIT; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- EXPLAIN statements +-- A Query is used, normalized by the query jumbling. +EXPLAIN (costs off) SELECT 1; +EXPLAIN (costs off) SELECT 2; +EXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 3; +EXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 7; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- CALL +CREATE OR REPLACE PROCEDURE sum_one(i int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + i)::int INTO r; +END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE sum_two(i int, j int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + j)::int INTO r; +END; $$ LANGUAGE plpgsql; +-- Overloaded functions. +CREATE OR REPLACE PROCEDURE overload(i int) AS $$ +DECLARE + r int; +BEGIN + SELECT (i + i)::int INTO r; +END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE overload(i text) AS $$ +DECLARE + r text; +BEGIN + SELECT i::text INTO r; +END; $$ LANGUAGE plpgsql; +-- Mix of IN/OUT parameters. +CREATE OR REPLACE PROCEDURE in_out(i int, i2 OUT int, i3 INOUT int) AS $$ +DECLARE + r int; +BEGIN + i2 := i; + i3 := i3 + i; +END; $$ LANGUAGE plpgsql; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +CALL sum_one(3); +CALL sum_one(199); +CALL sum_two(1,1); +CALL sum_two(1,2); +CALL overload(1); +CALL overload('A'); +CALL in_out(1, NULL, 1); +CALL in_out(2, 1, 2); +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +-- COPY +CREATE TABLE copy_stats (a int, b int); +SELECT edb_stat_statements_reset() IS NOT NULL AS t; +-- Some queries with A_Const nodes. +COPY (SELECT 1) TO STDOUT; +COPY (SELECT 2) TO STDOUT; +COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT; +COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT; +COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT; +COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT; +COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +DROP TABLE copy_stats; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- CREATE TABLE AS +-- SELECT queries are normalized, creating matching query IDs. +CREATE TABLE ctas_stats_1 AS SELECT 1 AS a; +DROP TABLE ctas_stats_1; +CREATE TABLE ctas_stats_1 AS SELECT 2 AS a; +DROP TABLE ctas_stats_1; +CREATE TABLE ctas_stats_2 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP TABLE ctas_stats_2; +CREATE TABLE ctas_stats_2 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 1; +DROP TABLE ctas_stats_2; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- CREATE MATERIALIZED VIEW +-- SELECT queries are normalized, creating matching query IDs. +CREATE MATERIALIZED VIEW matview_stats_1 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP MATERIALIZED VIEW matview_stats_1; +CREATE MATERIALIZED VIEW matview_stats_1 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3; +DROP MATERIALIZED VIEW matview_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- CREATE VIEW +CREATE VIEW view_stats_1 AS + SELECT a AS col1, 2::int AS col2 + FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2; +DROP VIEW view_stats_1; +CREATE VIEW view_stats_1 AS + SELECT a AS col1, 4::int AS col2 + FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3; +DROP VIEW view_stats_1; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Domains +CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0); +ALTER DOMAIN domain_stats SET DEFAULT '3'; +ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1); +DROP DOMAIN domain_stats; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Execution statements +SELECT 1 as a; +PREPARE stat_select AS SELECT $1 AS a; +EXECUTE stat_select (1); +DEALLOCATE stat_select; +PREPARE stat_select AS SELECT $1 AS a; +EXECUTE stat_select (2); +DEALLOCATE PREPARE stat_select; +DEALLOCATE ALL; +DEALLOCATE PREPARE ALL; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- SET statements. +-- These use two different strings, still they count as one entry. +CREATE ROLE regress_stat_set_1; +CREATE ROLE regress_stat_set_2; +SET work_mem = '1MB'; +Set work_mem = '1MB'; +SET work_mem = '2MB'; +SET work_mem = DEFAULT; +SET work_mem TO DEFAULT; +SET work_mem FROM CURRENT; +BEGIN; +SET LOCAL work_mem = '128kB'; +SET LOCAL work_mem = '256kB'; +SET LOCAL work_mem = DEFAULT; +SET LOCAL work_mem TO DEFAULT; +SET LOCAL work_mem FROM CURRENT; +COMMIT; +RESET work_mem; +SET enable_seqscan = off; +SET enable_seqscan = on; +SET SESSION work_mem = '300kB'; +SET SESSION work_mem = '400kB'; +RESET enable_seqscan; +-- SET TRANSACTION ISOLATION +BEGIN; +SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +COMMIT; +-- SET SESSION AUTHORIZATION +SET SESSION SESSION AUTHORIZATION DEFAULT; +SET SESSION AUTHORIZATION 'regress_stat_set_1'; +SET SESSION AUTHORIZATION 'regress_stat_set_2'; +RESET SESSION AUTHORIZATION; +BEGIN; +SET LOCAL SESSION AUTHORIZATION DEFAULT; +SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1'; +SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2'; +RESET SESSION AUTHORIZATION; +COMMIT; +-- SET SESSION CHARACTERISTICS +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY; +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY; +SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE; +-- SET XML OPTION +SET XML OPTION DOCUMENT; +SET XML OPTION CONTENT; +-- SET TIME ZONE +SET TIME ZONE 'America/New_York'; +SET TIME ZONE 'Asia/Tokyo'; +SET TIME ZONE DEFAULT; +SET TIME ZONE LOCAL; +SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0'; +RESET TIME ZONE; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; +DROP ROLE regress_stat_set_1; +DROP ROLE regress_stat_set_2; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- +-- Track the total number of rows retrieved or affected by the utility +-- commands of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED VIEW, +-- REFRESH MATERIALIZED VIEW and SELECT INTO +-- +CREATE TABLE pgss_ctas AS SELECT a, 'ctas' b FROM generate_series(1, 10) a; +SELECT generate_series(1, 10) c INTO pgss_select_into; +COPY pgss_ctas (a, b) FROM STDIN; +11 copy +12 copy +13 copy +\. +CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas; +REFRESH MATERIALIZED VIEW pgss_matv; +BEGIN; +DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv; +FETCH NEXT pgss_cursor; +FETCH FORWARD 5 pgss_cursor; +FETCH FORWARD ALL pgss_cursor; +COMMIT; + +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +DROP MATERIALIZED VIEW pgss_matv; +DROP TABLE pgss_ctas; +DROP TABLE pgss_select_into; + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; + +-- Special cases. Keep these ones at the end to avoid conflicts. +SET SCHEMA 'foo'; +SET SCHEMA 'public'; +RESET ALL; +SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE "C"; + +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/sql/wal.sql b/edb_stat_statements/sql/wal.sql new file mode 100644 index 00000000000..2555460514a --- /dev/null +++ b/edb_stat_statements/sql/wal.sql @@ -0,0 +1,20 @@ +-- +-- Validate WAL generation metrics +-- + +SET edb_stat_statements.track_utility = FALSE; + +CREATE TABLE pgss_wal_tab (a int, b char(20)); + +INSERT INTO pgss_wal_tab VALUES(generate_series(1, 10), 'aaa'); +UPDATE pgss_wal_tab SET b = 'bbb' WHERE a > 7; +DELETE FROM pgss_wal_tab WHERE a > 9; +DROP TABLE pgss_wal_tab; + +-- Check WAL is generated for the above statements +SELECT query, calls, rows, +wal_bytes > 0 as wal_bytes_generated, +wal_records > 0 as wal_records_generated, +wal_records >= rows as wal_records_ge_rows +FROM edb_stat_statements ORDER BY query COLLATE "C"; +SELECT edb_stat_statements_reset() IS NOT NULL AS t; diff --git a/edb_stat_statements/t/010_restart.pl b/edb_stat_statements/t/010_restart.pl new file mode 100644 index 00000000000..3a5dab06a6a --- /dev/null +++ b/edb_stat_statements/t/010_restart.pl @@ -0,0 +1,55 @@ +# Copyright (c) 2023-2024, PostgreSQL Global Development Group + +# Tests for checking that edb_stat_statements contents are preserved +# across restarts. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', + "shared_preload_libraries = 'edb_stat_statements'"); +$node->append_conf('postgresql.conf', + "edb_stat_statements.track_unrecognized = true"); +$node->start; + +$node->safe_psql('postgres', 'CREATE EXTENSION edb_stat_statements'); + +$node->safe_psql('postgres', 'CREATE TABLE t1 (a int)'); +$node->safe_psql('postgres', 'SELECT a FROM t1'); + +is( $node->safe_psql( + 'postgres', + "SELECT query FROM edb_stat_statements WHERE query NOT LIKE '%edb_stat_statements%' ORDER BY query" + ), + "CREATE TABLE t1 (a int)\nSELECT a FROM t1", + 'edb_stat_statements populated'); + +$node->restart; + +is( $node->safe_psql( + 'postgres', + "SELECT query FROM edb_stat_statements WHERE query NOT LIKE '%edb_stat_statements%' ORDER BY query" + ), + "CREATE TABLE t1 (a int)\nSELECT a FROM t1", + 'edb_stat_statements data kept across restart'); + +$node->append_conf('postgresql.conf', "edb_stat_statements.save = false"); +$node->reload; + +$node->restart; + +is( $node->safe_psql( + 'postgres', + "SELECT count(*) FROM edb_stat_statements WHERE query NOT LIKE '%edb_stat_statements%'" + ), + '0', + 'edb_stat_statements data not kept across restart with .save=false'); + +$node->stop; + +done_testing(); diff --git a/setup.py b/setup.py index d98045fabd8..a3353112526 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ EXT_LIB_DIRS = [ (ROOT_PATH / 'edb' / 'pgsql' / 'parser' / 'libpg_query').as_posix() ] +EDBSS_DIR = ROOT_PATH / 'edb_stat_statements' if platform.uname().system != 'Windows': @@ -195,7 +196,7 @@ def _get_env_with_openssl_flags(): return env -def _compile_postgres(build_base, *, +def _compile_postgres(build_base, build_temp, *, force_build=False, fresh_build=True, run_configure=True, build_contrib=True, produce_compile_commands_json=False): @@ -246,12 +247,21 @@ def _compile_postgres(build_base, *, if run_configure or fresh_build or is_outdated: env = _get_env_with_openssl_flags() - subprocess.run([ + cmd = [ str(postgres_src / 'configure'), '--prefix=' + str(postgres_build / 'install'), '--with-openssl', '--with-uuid=' + uuidlib, - ], check=True, cwd=str(build_dir), env=env) + ] + if os.environ.get('EDGEDB_DEBUG'): + cmd += [ + '--enable-tap-tests', + '--enable-debug', + ] + cflags = os.environ.get("CFLAGS", "") + cflags = f"{cflags} -O0" + env['CFLAGS'] = cflags + subprocess.run(cmd, check=True, cwd=str(build_dir), env=env) if produce_compile_commands_json: make = ['bear', '--', 'make'] @@ -279,6 +289,12 @@ def _compile_postgres(build_base, *, ['make', '-C', 'contrib', 'MAKELEVEL=0', 'install'], cwd=str(build_dir), check=True) + pg_config = ( + build_base / 'postgres' / 'install' / 'bin' / 'pg_config' + ).resolve() + _compile_pgvector(pg_config, build_temp) + _compile_edb_stat_statements(pg_config, build_temp) + with open(postgres_build_stamp, 'w') as f: f.write(source_stamp) @@ -289,7 +305,7 @@ def _compile_postgres(build_base, *, ) -def _compile_pgvector(build_base, build_temp): +def _compile_pgvector(pg_config, build_temp): git_rev = _get_git_rev(PGVECTOR_REPO, PGVECTOR_COMMIT) pgv_root = (build_temp / 'pgvector').resolve() @@ -317,10 +333,6 @@ def _compile_pgvector(build_base, build_temp): cwd=pgv_root, ) - pg_config = ( - build_base / 'postgres' / 'install' / 'bin' / 'pg_config' - ).resolve() - cflags = os.environ.get("CFLAGS", "") cflags = f"{cflags} {' '.join(SAFE_EXT_CFLAGS)} -std=gnu99" @@ -344,6 +356,27 @@ def _compile_pgvector(build_base, build_temp): ) +def _compile_edb_stat_statements(pg_config, build_temp): + subprocess.run( + [ + 'make', + f'PG_CONFIG={pg_config}', + ], + cwd=EDBSS_DIR, + check=True, + ) + + subprocess.run( + [ + 'make', + 'install', + f'PG_CONFIG={pg_config}', + ], + cwd=EDBSS_DIR, + check=True, + ) + + def _compile_libpg_query(): dir = (ROOT_PATH / 'edb' / 'pgsql' / 'parser' / 'libpg_query').resolve() @@ -387,13 +420,27 @@ def _get_git_rev(repo, ref): def _get_pg_source_stamp(): + from edb.buildmeta import hash_dirs + output = subprocess.check_output( ['git', 'submodule', 'status', '--cached', 'postgres'], universal_newlines=True, cwd=ROOT_PATH, ) revision, _, _ = output[1:].partition(' ') - source_stamp = revision + '+' + PGVECTOR_COMMIT + edbss_dir = EDBSS_DIR.as_posix() + edbss_hash = hash_dirs( + [(edbss_dir, '.c'), (edbss_dir, '.sql')], + extra_files=[ + EDBSS_DIR / 'Makefile', + EDBSS_DIR / 'edb_stat_statements.control', + ], + ) + edbss = binascii.hexlify(edbss_hash).decode() + stamp_list = [revision, PGVECTOR_COMMIT, edbss] + if os.environ.get('EDGEDB_DEBUG'): + stamp_list += ['debug'] + source_stamp = '+'.join(stamp_list) return source_stamp.strip() @@ -656,16 +703,13 @@ def run(self, *args, **kwargs): build = self.get_finalized_command('build') _compile_postgres( pathlib.Path(build.build_base).resolve(), + pathlib.Path(build.build_temp).resolve(), force_build=True, fresh_build=self.fresh_build, run_configure=self.configure, build_contrib=self.build_contrib, produce_compile_commands_json=self.compile_commands, ) - _compile_pgvector( - pathlib.Path(build.build_base).resolve(), - pathlib.Path(build.build_temp).resolve(), - ) class build_libpg_query(setuptools.Command): diff --git a/tests/test_edgeql_sys.py b/tests/test_edgeql_sys.py index 08921fb52db..db3fbb5ddfa 100644 --- a/tests/test_edgeql_sys.py +++ b/tests/test_edgeql_sys.py @@ -17,12 +17,77 @@ # +import asyncpg import edgedb +from edb.pgsql import common + from edb.testbase import server as tb -class TestEdgeQLSys(tb.QueryTestCase): +class TestQueryStatsMixin: + stats_magic_word: str = NotImplemented + stats_type: str = NotImplemented + + async def _query_for_stats(self): + raise NotImplementedError + + async def _bad_query_for_stats(self): + raise NotImplementedError + + async def _test_sys_query_stats(self): + stats_query = f''' + with stats := ( + select + sys::QueryStats + filter + .query like '%{self.stats_magic_word}%' + and .query not like '%sys::%' + and .query_type = $0 + ) + select sum(stats.calls) + ''' + calls = await self.con.query_single(stats_query, self.stats_type) + + await self._query_for_stats() + self.assertEqual( + await self.con.query_single(stats_query, self.stats_type), + calls + 1, + ) + + await self._bad_query_for_stats() + self.assertEqual( + await self.con.query_single(stats_query, self.stats_type), + calls + 1, + ) + + self.assertIsNone( + await self.con.query_single( + "select sys::reset_query_stats(branch_name := 'non_exdb')" + ) + ) + self.assertEqual( + await self.con.query_single(stats_query, self.stats_type), + calls + 1, + ) + + self.assertIsNotNone( + await self.con.query('select sys::reset_query_stats()') + ) + self.assertEqual( + await self.con.query_single(stats_query, self.stats_type), + 0, + ) + + +class TestEdgeQLSys(tb.QueryTestCase, TestQueryStatsMixin): + stats_magic_word = 'TestEdgeQLSys' + stats_type = 'EdgeQL' + SETUP = f''' + create type {stats_magic_word} {{ + create property bar -> str; + }}; + ''' async def test_edgeql_sys_locks(self): lock_key = tb.gen_lock_key() @@ -59,3 +124,42 @@ async def test_edgeql_sys_locks(self): 'select sys::_advisory_unlock($0)', lock_key), [False]) + + async def _query_for_stats(self): + self.assertEqual( + await self.con.query(f'select {self.stats_magic_word}'), + [], + ) + + async def _bad_query_for_stats(self): + async with self.assertRaisesRegexTx( + edgedb.InvalidReferenceError, 'does not exist' + ): + await self.con.query(f'select {self.stats_magic_word}_NoSuchType') + + async def test_edgeql_sys_query_stats(self): + await self._test_sys_query_stats() + + +class TestSQLSys(tb.SQLQueryTestCase, TestQueryStatsMixin): + stats_magic_word = 'TestSQLSys' + stats_type = 'SQL' + + async def _query_for_stats(self): + self.assertEqual( + await self.squery_values( + f"select {common.quote_literal(self.stats_magic_word)}" + ), + [[self.stats_magic_word]], + ) + + async def _bad_query_for_stats(self): + with self.assertRaisesRegex( + asyncpg.InvalidColumnReferenceError, "cannot find column" + ): + await self.squery_values( + f'select {self.stats_magic_word}_NoSuchType' + ) + + async def test_sql_sys_query_stats(self): + await self._test_sys_query_stats() diff --git a/tests/test_server_ops.py b/tests/test_server_ops.py index fd013d553b9..d8676304dde 100644 --- a/tests/test_server_ops.py +++ b/tests/test_server_ops.py @@ -42,6 +42,8 @@ import edgedb from edgedb import errors +import edb +from edb import buildmeta from edb import protocol from edb.common import devmode from edb.protocol import protocol as edb_protocol # type: ignore @@ -1698,3 +1700,39 @@ def reload_server(self): json.dump(self.conf, self.conf_file.file) self.conf_file.file.flush() self.srv.proc.send_signal(signal.SIGHUP) + + +class TestPGExtensions(tb.TestCase): + async def test_edb_stat_statements(self): + ext_home = ( + pathlib.Path(edb.__file__).parent.parent / 'edb_stat_statements' + ).resolve() + if not ext_home.exists(): + raise unittest.SkipTest("no source of edb_stat_statements") + with tempfile.TemporaryDirectory() as td: + cluster = await pgcluster.get_local_pg_cluster(td, log_level='s') + cluster.update_connection_params( + user='postgres', + database='template1', + ) + self.assertTrue(await cluster.ensure_initialized()) + await cluster.start(server_settings={ + 'edb_stat_statements.track_planning': 'false', + 'edb_stat_statements.track_unrecognized': 'true', + 'max_prepared_transactions': '5', + }) + try: + pg_config = buildmeta.get_pg_config_path() + env = os.environ.copy() + params = cluster.get_pgaddr() + env['PGHOST'] = params.host + env['PGPORT'] = params.port + env['PGUSER'] = params.user + env['PGDATABASE'] = params.database + subprocess.check_output([ + 'make', + f'PG_CONFIG={pg_config}', + 'installcheck', + ], cwd=str(ext_home), env=env) + finally: + await cluster.stop()