--- a +++ b/tests/autocomplete/autocomplete_definition.py @@ -0,0 +1,308 @@ +from ehrql import days, maximum_of, minimum_of, weeks +from ehrql.tables import core, emis, tpp + + +# A file to keep track of things where we get good autocomplete behaviour. +# +# Anything in this file is automatically checked by the test file test_autocomplete.py, +# though it must be formatted correctly or the tests will fail. Currently you can: +# +# - write a single command and confirm the type e.g.: +# core.patients.date_of_birth ## type:DatePatientSeries +# +# - assign a single command to a variable and confirm the type e.g.: +# bool_invert = ~core.patients.exists_for_patient() ## type:BoolPatientSeries +# +# - either of the above but spanning multiple lines e.g. +# bool_and = ( +# core.patients.exists_for_patient() & core.patients.exists_for_patient() +# ) ## type:BoolPatientSeries + +# Currently all columns on all tables have type of "Series | Any", +# so we don't get autocomplete. Providing type hints on the Series +# class, and on the @table decorator means the column types are +# correct e.g. IntPatientSeries, DateEventSeries etc. and therefore +# we get autocomplete for all the properties and methods on each series. +core.patients.date_of_birth ## type:DatePatientSeries +core.patients.sex ## type:StrPatientSeries +core.ons_deaths.underlying_cause_of_death ## type:CodePatientSeries + +core.clinical_events.date ## type:DateEventSeries +core.clinical_events.numeric_value ## type:FloatEventSeries +core.clinical_events.snomedct_code ## type:CodeEventSeries +tpp.apcs.all_diagnoses ## type:MultiCodeStringEventSeries + +tpp.addresses.has_postcode ## type:BoolEventSeries +tpp.addresses.address_id ## type:IntEventSeries + + +# There are some methods that always return the same type +core.clinical_events.snomedct_code.count_distinct_for_patient() ## type:IntPatientSeries +core.clinical_events.numeric_value.mean_for_patient() ## type:FloatPatientSeries +core.clinical_events.date.count_episodes_for_patient(weeks(1)) ## type:IntPatientSeries +core.patients.exists_for_patient() ## type:BoolPatientSeries +core.patients.count_for_patient() ## type:IntPatientSeries +bool_eq = days(100) == days(100) ## type:bool +bool_neq = days(100) != days(100) ## type:bool + +# There are some things that return the same type as the calling +# object or one of the arguments + +bool_and = ( + core.patients.exists_for_patient() & core.patients.exists_for_patient() +) ## type:BoolPatientSeries +bool_or = ( + core.patients.exists_for_patient() | core.patients.exists_for_patient() +) ## type:BoolPatientSeries +bool_invert = ~core.patients.exists_for_patient() ## type:BoolPatientSeries +numeric_neg = -core.clinical_events.numeric_value ## type:FloatEventSeries +core.clinical_events.date.to_first_of_year() ## type:DateEventSeries +core.patients.date_of_birth.to_first_of_month() ## type:DatePatientSeries +duration_negation = -days(100) ## type:days +core.patients.sex.when_null_then(core.patients.sex) ## type:StrPatientSeries +duration_add = days(100) + core.patients.date_of_birth ## type:DatePatientSeries +duration_radd = core.clinical_events.date + days(100) ## type:DateEventSeries +duration_add_duration = days(100) + days(100) ## type:days +# duration_rsub = core.clinical_events.date - days(100) ## type: DateEventSeries +# !!! the above doesn't work and thinks its a DateDifference. I assume +# !!! because the NotImplemented on the DateFunctions __sub__ method +# !!! is only known at runtime +# !!! commenting out until we fix it +duration_sub_duration = days(100) - days(100) ## type: days + + +# There are loads of things that return a series where the typ +# (int, str, float etc.) is fixed, but it can be a PatientSeries +# or an EventSeries depending on whether the calling object is a +# PatientSeries or an EventSeries. This can be achieved with two +# overloaded methods and type hints + +# +# BaseSeries +# +base_eq = core.patients.sex == core.patients.sex ## type:BoolPatientSeries +base_ne = ( + core.clinical_events.date != core.clinical_events.date +) ## type:BoolEventSeries +core.patients.sex.is_null() ## type:BoolPatientSeries +core.clinical_events.date.is_not_null() ## type:BoolEventSeries +core.patients.sex.is_in([]) ## type:BoolPatientSeries +core.clinical_events.snomedct_code.is_not_in([]) ## type:BoolEventSeries + +# +# ComparableFunctions +# +comparable_lt = ( + core.clinical_events.numeric_value < core.clinical_events.numeric_value +) ## type:BoolEventSeries +comparable_le = ( + core.clinical_events.numeric_value <= core.clinical_events.numeric_value +) ## type:BoolEventSeries +comparable_gt = ( + core.clinical_events.numeric_value > core.clinical_events.numeric_value +) ## type:BoolEventSeries +comparable_ge = ( + core.clinical_events.numeric_value >= core.clinical_events.numeric_value +) ## type:BoolEventSeries + +# +# StrFunctions +# +core.patients.sex.contains("m") ## type:BoolPatientSeries + +# +# NumericFunctions +# +numeric_truediv = core.clinical_events.numeric_value / 10 ## type:FloatEventSeries +numeric_rtruediv = 10 / core.clinical_events.numeric_value ## type:FloatEventSeries +numeric_floordiv = core.clinical_events.numeric_value // 10 ## type:IntEventSeries +numeric_rfloordiv = 10 // core.clinical_events.numeric_value ## type:IntEventSeries +core.clinical_events.numeric_value.as_int() ## type:IntEventSeries +core.clinical_events.numeric_value.as_float() ## type:FloatEventSeries + +# +# DateFunctions +# +date_str = "2024-01-01" +core.patients.date_of_birth.is_before(date_str) ## type:BoolPatientSeries +core.patients.date_of_birth.is_on_or_before(date_str) ## type:BoolPatientSeries +core.patients.date_of_birth.is_after(date_str) ## type:BoolPatientSeries +core.patients.date_of_birth.is_on_or_after(date_str) ## type:BoolPatientSeries +core.clinical_events.date.is_between_but_not_on( + date_str, date_str +) ## type:BoolEventSeries +core.clinical_events.date.is_on_or_between(date_str, date_str) ## type:BoolEventSeries +core.clinical_events.date.is_during((date_str, date_str)) ## type:BoolEventSeries + + +# +# MultiCodeStringFunctions +# +tpp.apcs.all_diagnoses.contains("N13") ## type:BoolEventSeries +tpp.apcs.all_diagnoses.contains_any_of(["N13"]) ## type:BoolEventSeries + +# +# Couple of random list[tuple] types +starting_on = weeks(3).starting_on("2000-01-01")[0][0] ## type:date +ending_on = weeks(3).ending_on("2000-01-01")[0][0] ## type:date + +# +# Things that aggregate from EventSeries to PatientSeries +# but that need to maintain the type (int, float, bool etc) +core.clinical_events.numeric_value.sum_for_patient() ## type:FloatPatientSeries +core.clinical_events.numeric_value.as_int().sum_for_patient() ## type:IntPatientSeries + +core.clinical_events.numeric_value.minimum_for_patient() ## type:FloatPatientSeries +core.clinical_events.numeric_value.as_int().minimum_for_patient() ## type:IntPatientSeries +tpp.addresses.msoa_code.minimum_for_patient() ## type:StrPatientSeries +core.clinical_events.date.minimum_for_patient() ## type:DatePatientSeries + +core.clinical_events.numeric_value.maximum_for_patient() ## type:FloatPatientSeries +core.clinical_events.numeric_value.as_int().maximum_for_patient() ## type:IntPatientSeries +tpp.addresses.msoa_code.maximum_for_patient() ## type:StrPatientSeries +core.clinical_events.date.maximum_for_patient() ## type:DatePatientSeries + +# +# NumericFunctions which maintain the series (Event or Patient) +# and the type (int or float) + +numeric_add = core.clinical_events.numeric_value + 10 ## type:FloatEventSeries +numeric_radd = 10 + core.clinical_events.numeric_value.as_int() ## type:IntEventSeries +numeric_add_patient = ( + core.clinical_events.numeric_value.maximum_for_patient() + 10 +) ## type:FloatPatientSeries +numeric_radd_patient = ( + 10 + core.clinical_events.numeric_value.as_int().maximum_for_patient() +) ## type:IntPatientSeries +numeric_add_series = ( + core.clinical_events.numeric_value + core.clinical_events.numeric_value +) ## type:FloatEventSeries + +numeric_sub = core.clinical_events.numeric_value - 10 ## type:FloatEventSeries +numeric_rsub = 10 - core.clinical_events.numeric_value.as_int() ## type:IntEventSeries +numeric_sub_patient = ( + core.clinical_events.numeric_value.maximum_for_patient() - 10 +) ## type:FloatPatientSeries +numeric_rsub_patient = ( + 10 - core.clinical_events.numeric_value.as_int().maximum_for_patient() +) ## type:IntPatientSeries +numeric_sub_series = ( + core.clinical_events.numeric_value - core.clinical_events.numeric_value +) ## type:FloatEventSeries + +numeric_mul = core.clinical_events.numeric_value * 10 ## type:FloatEventSeries +numeric_rmul = 10 * core.clinical_events.numeric_value.as_int() ## type:IntEventSeries +numeric_mul_patient = ( + core.clinical_events.numeric_value.maximum_for_patient() * 10 +) ## type:FloatPatientSeries +numeric_rmul_patient = ( + 10 * core.clinical_events.numeric_value.as_int().maximum_for_patient() +) ## type:IntPatientSeries +numeric_mul_series = ( + core.clinical_events.numeric_value * core.clinical_events.numeric_value +) ## type:FloatEventSeries + +# +# Horizontal aggregations +# The type checker casts eveything to the first series. But the only +# type we can easily get is the first arg. So if the first thing is +# a series then that's fine. Otherwise we ignore +# +max_of_float = maximum_of( + core.clinical_events.numeric_value, 10 +) ## type:FloatEventSeries +max_of_int = maximum_of( + core.clinical_events.numeric_value.as_int(), 10 +) ## type:IntEventSeries +max_of_date = maximum_of( + core.clinical_events.date, "2024-01-01" +) ## type:DateEventSeries +max_of_float_patient = maximum_of( + core.clinical_events.numeric_value.maximum_for_patient(), 10 +) ## type:FloatPatientSeries +max_of_int_patient = maximum_of( + core.clinical_events.numeric_value.maximum_for_patient().as_int(), 10 +) ## type:IntPatientSeries +max_of_date_patient = maximum_of( + core.patients.date_of_birth, "2024-01-01" +) ## type:DatePatientSeries +min_of_float = minimum_of( + core.clinical_events.numeric_value, 10 +) ## type:FloatEventSeries +min_of_int = minimum_of( + core.clinical_events.numeric_value.as_int(), 10 +) ## type:IntEventSeries +min_of_date = minimum_of( + core.clinical_events.date, "2024-01-01" +) ## type:DateEventSeries +min_of_float_patient = minimum_of( + core.clinical_events.numeric_value.minimum_for_patient(), 10 +) ## type:FloatPatientSeries +min_of_int_patient = minimum_of( + core.clinical_events.numeric_value.minimum_for_patient().as_int(), 10 +) ## type:IntPatientSeries +min_of_date_patient = minimum_of( + core.patients.date_of_birth, "2024-01-01" +) ## type:DatePatientSeries + +# properties +core.patients.date_of_birth.day ## type:IntPatientSeries +core.patients.date_of_birth.month ## type:IntPatientSeries +core.patients.date_of_birth.year ## type:IntPatientSeries +core.clinical_events.date.day ## type:IntEventSeries +core.clinical_events.date.month ## type:IntEventSeries +core.clinical_events.date.year ## type:IntEventSeries + +# Table methods not yet tested +tpp.patients.is_alive_on(date_str) ## type:BoolPatientSeries +tpp.patients.is_dead_on(date_str) ## type:BoolPatientSeries +tpp.decision_support_values.electronic_frailty_index() ## type:decision_support_values +tpp.practice_registrations.spanning_with_systmone( + date_str, date_str +) ## type:practice_registrations +emis.patients.has_practice_registration_spanning( + date_str, date_str +) ## type:BoolPatientSeries +core.patients.is_alive_on(date_str) ## type:BoolPatientSeries +core.patients.is_dead_on(date_str) ## type:BoolPatientSeries +core.practice_registrations.exists_for_patient_on(date_str) ## type:BoolPatientSeries +core.practice_registrations.spanning(date_str, date_str) ## type:practice_registrations + +# query_language series methods not yet tested +core.clinical_events.date.is_after(date_str) ## type: BoolEventSeries +core.clinical_events.date.is_before(date_str) ## type: BoolEventSeries +core.clinical_events.date.is_on_or_after(date_str) ## type: BoolEventSeries +core.clinical_events.date.is_on_or_before(date_str) ## type: BoolEventSeries +core.clinical_events.date.to_first_of_month() ## type: DateEventSeries +core.clinical_events.snomedct_code.is_in([]) ## type: BoolEventSeries +core.clinical_events.snomedct_code.is_null() ## type: BoolEventSeries +core.clinical_events.snomedct_code.is_null().as_int() ## type: IntEventSeries +core.clinical_events.snomedct_code.when_null_then(3) ## type: CodeEventSeries +core.patients.date_of_birth.day.as_float() ## type: FloatPatientSeries +core.patients.date_of_birth.day.as_int() ## type: IntPatientSeries +core.patients.date_of_birth.is_between_but_not_on( + date_str, date_str +) ## type: BoolPatientSeries +core.patients.date_of_birth.is_during((date_str, date_str)) ## type: BoolPatientSeries +core.patients.date_of_birth.is_on_or_between( + date_str, date_str +) ## type: BoolPatientSeries +core.patients.date_of_birth.to_first_of_year() ## type: DatePatientSeries +core.patients.sex.is_not_in(["male"]) ## type: BoolPatientSeries +core.patients.sex.is_not_null() ## type: BoolPatientSeries +core.patients.sex.is_null().as_int() ## type: IntPatientSeries +tpp.apcs.all_diagnoses.is_in([]) ## type:NoReturn +tpp.apcs.all_diagnoses.is_not_in([]) ## type:NoReturn +tpp.addresses.msoa_code.contains([]) ## type: BoolEventSeries + +# query_language non-series methods +core.clinical_events.where( + core.clinical_events.snomedct_code.is_in([]) +) ## type:clinical_events +core.clinical_events.except_where( + core.clinical_events.snomedct_code.is_in([]) +) ## type:clinical_events + +# Duration methods +days(100).starting_on("2045-01-01") ## type: list[tuple[date, date]] +days(100).ending_on("2045-01-01") ## type: list[tuple[date, date]]