- Home
- LLBLGen Pro
- LLBLGen Pro Runtime Framework
New problem with postgres int array colums
Joined: 12-Jun-2007
Hi all,
IIRC up to recently LLBLGen detected array and json columns in a postgres table as varchar(max) and treated the content of theses columns as text. The designer still follows this scheme, but unfortunately this does't work any longer in the runtime.
I've no idea when this behaviour started to change, but with current LLBLGen 5.8.1 and NpgSql 5.0.5 the getter of the following property crashes. I've only noticed it today.
/// <summary>The CostelloDistinctSiteAuthorIds property of the Entity SiteAuthorName<br/><br/></summary>
/// <remarks>Mapped on table field: "SiteAuthorName"."CostelloDistinctSiteAuthorIds".<br/>Table field type characteristics (type, precision, scale, length): Varchar, 0, 0, 10485760.<br/>Table field behavior characteristics (is nullable, is PK, is identity): false, false, false</remarks>
public virtual System.String CostelloDistinctSiteAuthorIds
{
get { return (System.String)GetValue((int)SiteAuthorNameFieldIndex.CostelloDistinctSiteAuthorIds, true); }
set { SetValue((int)SiteAuthorNameFieldIndex.CostelloDistinctSiteAuthorIds, value); }
}
The column CostelloDistinctSiteAuthorIds is an int[] colum in the underlying postgres table and
GetValue((int)SiteAuthorNameFieldIndex.CostelloDistinctSiteAuthorIds, true);
now actually returns an int[] making the cast to System.String cause a crash.
Trying to set the value with a string e.g. "{1,2,3,4}" equally leads to a crash, with the message that string cannot be type casted to int[].
@Frans, can you take a look please? TypeConverter is no valid solution to the problem, as an int[] is neither an accepted type for in- nor output in the designer.
If it is merely a problem with the latest NpgSql driver, can you recommend a working version?
Cheers, kamiwa
=========================
P.S.: Downgrading NpgSql to 4.0.12 doesn't fix the problem. I've just tried.
Joined: 17-Aug-2003
What I don't understand is that it previously did work? Who did the int[] -> string conversion? The thing is that we don't support array typed fields, so the driver falls back to the default type, varchar, so the generated code simply uses string. I checked whether Npgsql had a connection string argument to make this work but this isn't the case. I also can't find any breaking change for npgsql or other change in this area.
If this has worked before, then something did the int[]-> string conversion but it wasn't us. I also can't find anything related to this. Something changed, but we didn't change anything in this area.
Joined: 12-Jun-2007
Otis wrote:
What I don't understand is that it previously did work? Who did the int[] -> string conversion? The thing is that we don't support array typed fields, so the driver falls back to the default type, varchar, so the generated code simply uses string. I checked whether Npgsql had a connection string argument to make this work but this isn't the case. I also can't find any breaking change for npgsql or other change in this area.
If this has worked before, then something did the int[]-> string conversion but it wasn't us. I also can't find anything related to this. Something changed, but we didn't change anything in this area.
Frans, I'm currently busy with another problem. I'll get back to you later, OK?
Joined: 12-Jun-2007
Otis wrote:
What I don't understand is that it previously did work? Who did the int[] -> string conversion? The thing is that we don't support array typed fields, so the driver falls back to the default type, varchar, so the generated code simply uses string. I checked whether Npgsql had a connection string argument to make this work but this isn't the case. I also can't find any breaking change for npgsql or other change in this area.
If this has worked before, then something did the int[]-> string conversion but it wasn't us. I also can't find anything related to this. Something changed, but we didn't change anything in this area.
I don't understand it either. I just guessed that it did work before, because in the plenty of postgres databases that we target here with LLBLGen ORM, there are quite a few tables that do use more exotic types than primitives, and it's unlikely that those tables weren't used in code.
But unlikely of course doesn't mean that it is impossible. So let us assume for now, that it has always been like this.
Let me come back to using a TypeConverter:
As I've just stated in the other thread,
The problem with the TypeConverter approach is, that if you write a type converter that converts from string to int[] and vice versa, you would need to be able to pick System.Int32[] (aka int[]) as .NET datatype for the entity's corresponding property. Unfortunatly appart from byte[] there is no array type supported in the LLBLGen Designer GUI.
What is it, that I'm not seeing?
Joined: 17-Aug-2003
kamiwa wrote:
Otis wrote:
What I don't understand is that it previously did work? Who did the int[] -> string conversion? The thing is that we don't support array typed fields, so the driver falls back to the default type, varchar, so the generated code simply uses string. I checked whether Npgsql had a connection string argument to make this work but this isn't the case. I also can't find any breaking change for npgsql or other change in this area.
If this has worked before, then something did the int[]-> string conversion but it wasn't us. I also can't find anything related to this. Something changed, but we didn't change anything in this area.
I don't understand it either. I just guessed that it did work before, because in the plenty of postgres databases that we target here with LLBLGen ORM, there are quite a few tables that do use more exotic types than primitives, and it's unlikely that those tables weren't used in code.
But unlikely of course doesn't mean that it is impossible. So let us assume for now, that it has always been like this.
Things don't magically change tho Perhaps it's a postgresql setting, I don't know, to be honest, nor what to do in this case, as I don't see a solution, as all we do is pipe through the value we get from the datareader.
Let me come back to using a TypeConverter:
As I've just stated in the other thread,
The problem with the TypeConverter approach is, that if you write a type converter that converts from string to int[] and vice versa, you would need to be able to pick System.Int32[] (aka int[]) as .NET datatype for the entity's corresponding property. Unfortunatly appart from byte[] there is no array type supported in the LLBLGen Designer GUI.
What is it, that I'm not seeing?
Byte[] is a type that's specified in one of the drivers, so it's in the list of types, but you can return any type in the type converter and it's used as-is. If your type converter's type is int[] (so it instantiates an int[]) the designer will set the field to that type. Saving it will likely be a problem as npgsql requires special settings on the parameter for arrays (IIRC).
But I'm still baffled that it has worked at all. Somewhere there has been a magical string.Join(",", arrayValue) call
Joined: 12-Jun-2007
Otis wrote:
Byte[] is a type that's specified in one of the drivers, so it's in the list of types, but you can return any type in the type converter and it's used as-is. If your type converter's type is int[] (so it instantiates an int[]) the designer will set the field to that type.
Unfortunately this doesn't work for me. Here's the code for two type converters.
The first one converts string to object an vice versa.
using System;
using System.ComponentModel;
namespace SD.LLBLGen.Pro.CustomTypeConverters {
public class StringObjectConverter : TypeConverter {
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string);
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) => destinationType == typeof(object);
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) {
// value is the value that gets returned from the NpgSqlReader;
return value;
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) {
// value is the value that gets passed to the NpgSqlReader;
return value;
}
public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) {
return new object();
}
}
}
The second one converts string to int array.
using System;
using System.ComponentModel;
namespace SD.LLBLGen.Pro.CustomTypeConverters {
public class StringIntArrayConverter : TypeConverter {
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string);
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) => destinationType == typeof(int[]);
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) {
// value is the value that gets returned from the NpgSqlReader and is already an int[];
return value is int[] intArray ? intArray : null;
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) {
// value is the value that gets returned from the entity and is already an int[];
return value is int[] intArray ? intArray : null;
}
public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) {
return Array.Empty<int>();
}
}
}
If I "register" both of them under Projects -> Settings -> Conventions -> Entity Model -> Type Conversions, the LLBLGen Designer will let me pick the StringObjectConverter if prior to assigning the TypeConverter under FieldMappins I set the type of the field to System.Object under the Fields tab.
If I leave the type of the field as is (System.String) I won't be presented with any assignable TypeConverter at all. Setting the type of the field to object, I can pick the StringObject only. If I add a new dummy field under Fields and leave the .NET Type unassigned I can pick one from any of the included TypeConverters plus the two I registered.
So if you wanted to pick the StringArrayConverter you'd need to change the type of the field to int[] before you can do so. And as there is now way of doing so, you can't use it at all. To me it seems, that select box is filtered by both types: the DBType and the target type in the entity.
Joined: 17-Aug-2003
Oh of course, I overlooked the fact the field type in the DB is a string too.... One workaround could be to use a type converter to convert string <->string and internally in ConvertFrom
pass the value as-is if it's a string, and if it's an int[], return string.Join(",", value);
. This will keep the field a string field in the entity, and will allow you to set the type converter on the mapping as the database field is a string (according to the meta-data, as the driver thinks it is a string), and at runtime it will make sure the value is converted to a string from the int[] it gets from the datareader.
I've checked all code in the drivers and runtime for postgresql but nothing changed in the last couple of versions so that can't be it. I've asked Shay Rojansky (Lead dev of npgsql) if he knows something related to this, e.g. whether it was a feature in npgsql that was recently removed or has to be explicitly enabled. Will keep you posted what I hear back...
Joined: 17-Aug-2003
Shay says npgsql never did any array -> string conversions so npgsql never returned a string for this type.
What I think might have happened is that you now read the field and before you didn't, so it now pops up as an invalid cast while before you didn't notice as the field was never read? Could that be the case?
Joined: 12-Jun-2007
Otis wrote:
Oh of course, I overlooked the fact the field type in the DB is a string too.... One workaround could be to use a type converter to convert string <->string and internally in
ConvertFrom
pass the value as-is if it's a string, and if it's an int[], returnstring.Join(",", value);
. This will keep the field a string field in the entity, and will allow you to set the type converter on the mapping as the database field is a string (according to the meta-data, as the driver thinks it is a string), and at runtime it will make sure the value is converted to a string from the int[] it gets from the datareader.I've checked all code in the drivers and runtime for postgresql but nothing changed in the last couple of versions so that can't be it. I've asked Shay Rojansky (Lead dev of npgsql) if he knows something related to this, e.g. whether it was a feature in npgsql that was recently removed or has to be explicitly enabled. Will keep you posted what I hear back...
In that case I rather stick with the StringObjectConverter. This has the adavantage that I get the int[] from the NpgSqlReader in the the getter of the Property and it seems that when you change the ElementFieldMapping in the PersistenceInfoProvider from
this.AddElementFieldMapping("SiteAuthorNameEntity", "CostelloDistinctSiteAuthorIds", "CostelloDistinctSiteAuthorIds", false, "Varchar", 10485760, 0, 0, false, "", new SD.LLBLGen.Pro.CustomTypeConverters.StringObjectConverter(), typeof(System.String), 3);
to
this.AddElementFieldMapping("SiteAuthorNameEntity", "CostelloDistinctSiteAuthorIds", "CostelloDistinctSiteAuthorIds", false, "int[]", 10485760, 0, 0, false, "", new SD.LLBLGen.Pro.CustomTypeConverters.StringObjectConverter(), typeof(System.String), 3);
you can actually set the value with an int[] value and the adapter will even save it.
At least last night I succeed in doing so. Will have to check, whether this still works, because last night I dived into the source code of the SD.LLBLGen.Pro.DQE.PostgreSql and SD.LLBLGen.Pro.ORMSupportClasses and made two changes that simply blocked assign a type to the DbParameter when the value for the parameter is an array. NpgSql driver ist then clever enough to detect the correct parameter type.
I'll keep you posted, whether the string->object converter in combination with the change in the PersistenceInfoProvider (VarChar => Int[]) and without the changes in the LLBLGen framework does work.
I think that changing "VarChar" to "int[]" in the PersistenceInfoProvider works, because your code doesn't find a matching DbType type and thus doesn't assign a Type to the parameter either.
The int[] to string way is definetly no solution, as this leads to a crash in NpgSql with the message being that the type of the field is an int[] array which can't be updated with a Texthandler (or something alike).
Joined: 12-Jun-2007
Otis wrote:
Shay says npgsql never did any array -> string conversions so npgsql never returned a string for this type.
What I think might have happened is that you now read the field and before you didn't, so it now pops up as an invalid cast while before you didn't notice as the field was never read? Could that be the case?
As I said: I just assumed that it worked, because I never had any problems with int[] fields so far. Perhaps, as you said above, those properties where never touched in any C# code using LLBLGen and the entities having int[] fields were never saved or updated with LLBLGen code.
I can't rule it out.
A lot of coding these days is nowadays done here in Python. So perhaps we were just lucky so far and entity updates with int array fields were handled by Python code.
Joined: 17-Aug-2003
All things considered I indeed think that property was never read. The string route is one that's dictated because the driver falls back to varchar (as it doesn't recognize the type) and of course far from ideal.
Changing the persistence info provider by hand is not really ideal, as it's generated code. Detecting the array as a type is done in PostgresqlDBDriver :: ConvertStringToDBType, however this isn't straightforward as there are no entries in the type mapping tables for an int array.
Adding it on our side opens a big can of worms as array typed fields can be used in queries in theory and we don't have the APIs for that, hence we held off that boat.
Joined: 12-Jun-2007
Otis wrote:
All things considered I indeed think that property was never read. The string route is one that's dictated because the driver falls back to varchar (as it doesn't recognize the type) and of course far from ideal.
Changing the persistence info provider by hand is not really ideal, as it's generated code. Detecting the array as a type is done in PostgresqlDBDriver :: ConvertStringToDBType, however this isn't straightforward as there are no entries in the type mapping tables for an int array.
Adding it on our side opens a big can of worms as array typed fields can be used in queries in theory and we don't have the APIs for that, hence we held off that boat.
Well: If you've got a can of worms and a boat, go fishing!
Joined: 17-Aug-2003
Heh Good point.
So back to the problem at hand, we can't 'enable' it on the designer level for you at this stage, so I'm afraid it's back to 'not supported' territory. Not what you wanted to hear I guess, but other than that I don't think I have a lot of other options to offer.
Joined: 12-Jun-2007
Got it working in so far as I now can read and write array columns. Should work for string[] as well.
I've used the StringObjectConverter from above. In the LLBLGen Designer I've changed the .NET Type for the int[] model field to Object and assigned the TypeConverter.
In the created code I've modified the generated code in PersistenceInfoProvider.cs for the underlying entity from
this.AddElementFieldMapping("SiteAuthorNameEntity", "CostelloDistinctSiteAuthorIds", "CostelloDistinctSiteAuthorIds", false, "Varchar", 10485760, 0, 0, false, "", new SD.LLBLGen.Pro.CustomTypeConverters.StringObjectConverter(), typeof(System.String), 3);
to
this.AddElementFieldMapping("SiteAuthorNameEntity", "CostelloDistinctSiteAuthorIds", "CostelloDistinctSiteAuthorIds", false, "Array", 10485760, 0, 0, false, "", new SD.LLBLGen.Pro.CustomTypeConverters.StringObjectConverter(), typeof(System.String), 3);
So it's just "Varchar" replaced by "Array"
(As you already said: Altering generated code is not very clever, but I don't see any other way. Or ist there a way to alter the actual DotNetType from code?)
In a next step I've removed any reference to the SD.LLBLGen.Pro.DQE.PostgreSql nuget package from my solution and replaced it with a refence to its source code.
In the source of the PostgreSqlSpecificCreator class I replaced the method SetParameterType with the following code:
protected override void SetParameterType(DbParameter parameter, string parameterType) {
if (string.IsNullOrEmpty(parameterType)) {
return;
}
if (parameterType.Equals("Array")) {
return;
}
_dbProviderFactoryInfo.SetParameterType(parameter, parameterType);
}
That's it.
Testcode:
try {
using var adapter = new RubiconDataAccessAdapter();
var entity = new SiteAuthorNameEntity(authorId);
adapter.FetchEntity(entity);
// Works as expected
entity.CostelloDistinctSiteAuthorIds = new[] { 1,2,3,4,5 };
adapter.SaveEntity(entity, true);
/*
Works as expected
Executed Sql Query:
Query: UPDATE "public"."SiteAuthorName" SET "CostelloDistinctSiteAuthorIds"=:p1 WHERE ( "public"."SiteAuthorName"."Id" = :p2)
Parameter: :p1 : Object. Length: 10485760. Precision: 0. Scale: 0. Direction: Input. Value: binary lob.
Parameter: :p2 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "adriennebraun-20897".
*/
entity = new SiteAuthorNameEntity {
Id = $"xxxxxxxxxxxxxxxxx-{entity.SiteId}",
CleanAuthorName = "xxxxxxxxxxxxxxxxx",
SiteId = entity.SiteId,
SiteName = entity.SiteName,
HostName = entity.HostName,
SiteUrl = entity.SiteUrl,
AuthorName = entity.AuthorName,
FirstName = entity.FirstName,
MiddleName = entity.MiddleName,
LastName = entity.LastName,
NamePartsCount = entity.NamePartsCount,
CostelloDistinctSiteAuthorIds = entity.CostelloDistinctSiteAuthorIds,
NameVariantCount = entity.NameVariantCount,
ArticleCount = entity.ArticleCount,
MaxArticleCount = entity.MaxArticleCount
};
adapter.SaveEntity(entity, true);
/*
Works as expected
Executed Sql Query:
Query: INSERT INTO "public"."SiteAuthorName" ("ArticleCount", "AuthorName", "CleanAuthorName", "CostelloDistinctSiteAuthorIds", "FirstName", "HostName", "Id", "LastName", "MaxArticleCount", "MiddleName", "NamePartsCount", "NameVariantCount", "SiteId", "SiteName", "SiteUrl") VALUES (:p1, :p2, :p3, :p4, :p5, :p6, :p7, :p8, :p9, :p10, :p11, :p12, :p13, :p14, :p15)
Parameter: :p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 32.
Parameter: :p2 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "Adrienne Braun".
Parameter: :p3 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "xxxxxxxxxxxxxxxxx".
Parameter: :p4 : Object. Length: 10485760. Precision: 0. Scale: 0. Direction: Input. Value: binary lob.
Parameter: :p5 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "Adrienne".
Parameter: :p6 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "schwarzwaelder-bote.de".
Parameter: :p7 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "xxxxxxxxxxxxxxxxx-20897".
Parameter: :p8 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "Braun".
Parameter: :p9 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 32.
Parameter: :p10 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "".
Parameter: :p11 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 2.
Parameter: :p12 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
Parameter: :p13 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 20897.
Parameter: :p14 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "Schwartzwaelder-bote".
Parameter: :p15 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "https://www.schwarzwaelder-bote.de".
*/
adapter.DeleteEntity(entity);
/*
Works as expected
Executed Sql Query:
Query: DELETE FROM "public"."SiteAuthorName" WHERE ( "public"."SiteAuthorName"."Id" = :p1)
Parameter: :p1 : String. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "xxxxxxxxxxxxxxxxx-20897".
*/
entity = new SiteAuthorNameEntity(authorId);
adapter.FetchEntity(entity);
var result = new {
entity.Id,
entity.AuthorName,
entity.CostelloDistinctSiteAuthorIds
};
return new JsonResult(result);
}
catch (Exception ex) {
return handleException(ex);
}
Ah, and yes, let's hope that nobody wants to use the array fields in a predicate expression.
Joined: 17-Aug-2003
Hmm, still a lot of manual steps. Sadly we can't change it at this point: they're currently unsupported. Another route for you might be to use a customized postgresql driver for llblgen (changed from the sourcecode in teh source archive), so you report an int[] array etc. as a System.Array so code generation picks that up, but that too will be pretty brittle in the long run. We'll see what we can do in the future.
Joined: 12-Jun-2007
Otis wrote:
Hmm, still a lot of manual steps.
To eliminate the step of manually altering the generated PersistenceInfo code, here's a new static method in my overridden DataAccessAdapter class. Cannot connect to any of our postgres databases with the default DataAccessAdapter as we require client certificates for connecting.
public static void SetPersistenceInfo() {
var type = typeof(PersistenceInfoProviderCore);
FieldInfo fieldInfo = null;
while (type != null) {
fieldInfo = type.GetField("_elementMappings", BindingFlags.Instance | BindingFlags.NonPublic);
if (fieldInfo != null) {
break;
}
type = type.BaseType;
}
if (fieldInfo == null) {
return;
}
var persistenceInfoProvider = PersistenceInfoProviderSingleton.GetInstance();
var dict = fieldInfo.GetValue(persistenceInfoProvider) as IDictionary;
var keys = dict?.Keys;
if(keys == null) {
return;
}
var sourceColumnDbTypeField =
typeof(FieldPersistenceInfo).GetField("_sourceColumnDbType", BindingFlags.Instance | BindingFlags.NonPublic);
if (sourceColumnDbTypeField == null) {
return;
}
foreach (var key in keys) {
var fieldPersistenceInfos = persistenceInfoProvider.GetAllFieldPersistenceInfos(key.ToString());
foreach (var fieldPersistenceInfo in fieldPersistenceInfos) {
if(!(fieldPersistenceInfo.TypeConverterToUse is StringObjectConverter)) {
continue;
}
if (fieldPersistenceInfo.SourceColumnDbType.Equals("Array")) {
continue;
}
sourceColumnDbTypeField.SetValue(fieldPersistenceInfo, "Array");
}
}
}
Otis wrote:
Another route for you might be to use a customized postgresql driver for llblgen (changed from the sourcecode in teh source archive), so you report an int[] array etc. as a System.Array so code generation picks that up, but that too will be pretty brittle in the long run. We'll see what we can do in the future.
Already took a look into it, and agree that in the long run that'll be the route to go.
Joined: 12-Jun-2007
Otis wrote:
Done It's shared static info with no locks whatsoever (as it's considered readonly), so be sure to run this method before any persistence taking place.
Got it!
Done <= Refers to the thread being moved to public area
It's shared static info with no locks whatsoever (as it's considered readonly), so be sure to run this method before any persistence taking place. <= Hint for using the code.
Too old to rock'n'roll but too young to die.
Joined: 12-Jun-2007
In the meantime I've started trying to tweak the PostgresqlDBDriver to my needs.
A few posts further above
Otis wrote:
Detecting the array as a type is done in PostgresqlDBDriver :: ConvertStringToDBType, however this isn't straightforward as there are no entries in the type mapping tables for an int array.
==> ... as there are no entries in the type mapping tables for an int array.
In the LLBLGen Designer you rely on the RetrieveTableAndFieldMetaData and RetrieveViewAndFieldMetaData methods in the PostgreSqlSchemaRetriever class from the PostgreSqlDbDriver project in order to get the columns and column types of a table you're using the following query:
SELECT
c.*,
COL_DESCRIPTION(t.oid, c.ordinal_position) AS column_comment
FROM
INFORMATION_SCHEMA.COLUMNS c
INNER JOIN pg_class t
ON c.table_name = t.relname
INNER JOIN pg_namespace s
ON c.table_schema = s.nspname AND s.oid = t.relnamespace
WHERE
table_schema = '{0}' AND
table_name = :tableName
ORDER BY
ordinal_position ASC;
Then you get the type of the column from the content in the data_type colum from INFORMATION_SCHEMA.COLUMNS.
While it is true, that any colum of type array will only have ARRAY as the assigned type in this column, you can additionally look at the udt_name column which will tell you exactly of what child-type the array is.
For an array of text, the udt_name column will state _text. For an array of int4, the udt_name column will state _int4.
So in order to detect the complete type of an Array or a Range column you need both columns.
Type declaration in NpgSQL ADO driver follows the same rule.
From https://www.npgsql.org/doc/types/basic.html#write-mappings
Note: When using Range and Array, bitwise-or NpgsqlDbType.Range or NpgsqlDbType.Array with the child type. For example, to construct the NpgsqlDbType for a int4range, write NpgsqlDbType.Range | NpgsqlDbType.Integer. To construct the NpgsqlDbType for an int[], write NpgsqlDbType.Array | NpgsqlDbType.Integer.
So in order to get the the complete type I've modified the sql query to:
SELECT
table_catalog,
table_schema,
table_name,
column_name,
ordinal_position,
column_default,
is_nullable,
case WHEN data_type = 'ARRAY' then 'ARRAY' || udt_name else data_type END as data_type,
character_maximum_length,
character_octet_length,
numeric_precision,
numeric_precision_radix,
numeric_scale,
datetime_precision,
interval_type,
interval_precision,
character_set_catalog,
character_set_schema,
character_set_name,
collation_catalog,
collation_schema,
collation_name,
domain_catalog,
domain_schema,
domain_name,
udt_catalog,
udt_schema,
udt_name,
scope_catalog,
scope_schema,
scope_name,
maximum_cardinality,
dtd_identifier,
is_self_referencing,
is_identity,
identity_generation,
identity_start,
identity_increment,
identity_maximum,
identity_minimum,
identity_cycle,
is_generated,
generation_expression,
is_updatable,
COL_DESCRIPTION(t.oid, c.ordinal_position) AS column_comment
FROM
INFORMATION_SCHEMA.COLUMNS c
INNER JOIN pg_class t
ON c.table_name = t.relname
INNER JOIN pg_namespace s
ON c.table_schema = s.nspname AND s.oid = t.relnamespace
WHERE
table_schema = 'public' AND
table_name = 'CostelloDistinctSiteAuthorAggregation'
ORDER BY
ordinal_position ASC;
Let's see where this road will take me.