Package: pydantic-surql
Pydantic-SURQLPydantic Surql is a utility set to automatically convert Pydantic models to SURQL SDL definitions.✅ it supports the following featurescollection definitions (a.k.a tables)schemafull / schemalessdropchangefeedviewindexes definitionsregular indexesunique indexessearch indexesfields and tables permissionsanalyzers definitiontokenizers definitionevents definitionand thefollowing typesout of the box :basic types (string | int | float | boolean | datetime | any, enum)unionoptional and nullarraysetobjectrecord❌ what it doesn't support yet :Future typesGeometry typesSURQL queries definitions (select, update, delete, where, ...)SURQL functions definitionsSURQL scopes definitionsSURQL SDL field validation (validation goes through pydantic, too complex at this stage)namespaces definitionsdatabases definitionsusers definitionstokens definitionsparams definitionsPRs are welcome 😉installationTo install pydantic-surql run :pipinstallpydantic-surqlor with poetry :poetryaddpydantic-surqlbasic usageto convert a pydantic model to a surql SDL definition you can use a simple decorator :fromdatetimeimportdatetimefromtypingimportOptionalfrompydanticimportBaseModelfrompydantic_surqlimportsurql_collection@surql_collection("writers")classWriter(BaseModel):id:strfirstname:strlastname:strbirthdate:datetime@surql_collection("books")classBook(BaseModel):id:strtitle:strpages:Optional[int]description:strweight:floatwriter:WriterAll the models decorated with@surql_collectionwill be collected by theMetadataobject, which will be used to generate the SDL.To generate the SDL :frompydantic_surqlimportMetadatamodels_sdl:str=Metadata.collect()this will generate the following SDL :DEFINE TABLE writers SCHEMAFULL;
DEFINE FIELD firstname ON TABLE writers TYPE string;
DEFINE FIELD lastname ON TABLE writers TYPE string;
DEFINE FIELD birthdate ON TABLE writers TYPE datetime;

DEFINE TABLE books SCHEMAFULL;
DEFINE FIELD title ON TABLE books TYPE string;
DEFINE FIELD pages ON TABLE books TYPE option<number>;
DEFINE FIELD description ON TABLE books TYPE string;
DEFINE FIELD weight ON TABLE books TYPE number;
DEFINE FIELD writer ON TABLE books TYPE record<writers>;Collections definitionsschemafull / schemaless definitionsBy default collections are schemafull because pydantic doesn't allow models to have extra values.to make a collection schemaless you can usepydantic built in feature.frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel,ConfigDict@surql_collection("schemaless_collection")classSchemaLessCollection(BaseModel):model_config=ConfigDict(extra='allow')#...print(Metadata.collect())orfrompydantic_surqlimportsurql_collection,Metadatafrompydantic_surql.typesimportSurQLTableConfigfrompydanticimportBaseModel,ConfigDict@surql_collection("schemaless_collection",SurQLTableConfig(strict=False))classSchemaLessCollection(BaseModel):passprint(Metadata.collect())[!NOTE]
ifmodel_config.extra == "allow", thetable.config.strictwill be set totrueifstrict == False, themodel_config.extrawill be set toallowthis will generate the following SDL :DEFINE TABLE schemaless_collection SCHEMALESS;drop definitionsyou can define the collection as dropped through the config :frompydantic_surqlimportsurql_collection,Metadatafrompydantic_surql.typesimportSurQLTableConfigfrompydanticimportBaseModel@surql_collection("drop_collection",SurQLTableConfig(drop=True))classDropCollection(BaseModel):passprint(Metadata.collect())this will generate the following SDL :DEFINE TABLE drop_collection DROP SCHEMAFULL;changefeed definitionsyou can define the changefeed on collection through the config :frompydantic_surqlimportsurql_collection,Metadatafrompydantic_surql.typesimportSurQLTableConfigfrompydanticimportBaseModel@surql_collection("changefeed_collection",SurQLTableConfig(changeFeed="1d"))classChangefeedCollection(BaseModel):passprint(Metadata.collect())this will generate the following SDL :DEFINE TABLE changefeed_collection SCHEMAFULL CHANGEFEED 1d;view definitionsyou can define a collection as a view through the config :frompydantic_surqlimportsurql_collection,Metadatafrompydantic_surql.typesimportSurQLTableConfig,SurQLViewfrompydanticimportBaseModelconfig=SurQLTableConfig(asView=SurQLView(select=["name","age"],from_t=["users"],where=["age > 18"],group_by=["age"]))@surql_collection("view_collection",config)classViewCollection(BaseModel):name:list[str]age:strprint(Metadata.collect())this will generate the following SDL :DEFINE TABLE view_collection AS SELECT name,age FROM users WHERE age > 18 GROUP BY age;indexes, analyzers and tokenizers definitionsYou can define indexes on collections through the config :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel,ConfigDictfrompydantic_surql.typesimport(SurQLTableConfig,SurQLIndex,SurQLUniqueIndex,SurQLSearchIndex,SurQLAnalyzer,SurQLTokenizers)index=SurQLIndex(name="index_name",fields=["field1","field2"])unique_index=SurQLUniqueIndex(name="unique_index_name",fields=["field1","field2"])analyzer=SurQLAnalyzer(name="analyzer_name",tokenizers=[SurQLTokenizers.BLANK])search_index=SurQLSearchIndex(name="search_index_name",fields=["field3"],analyzer=analyzer,highlights=True)@surql_collection("indexed_collection",SurQLTableConfig(indexes=[index,unique_index,search_index]))classIndexedCollection(BaseModel):field1:strfield2:strfield3:strprint(Metadata.collect())this will generate the following SDL :DEFINE ANALYZER analyzer_name TOKENIZERS blank;

DEFINE TABLE indexed_collection SCHEMAFULL;
DEFINE FIELD field1 ON TABLE indexed_collection TYPE string;
DEFINE FIELD field2 ON TABLE indexed_collection TYPE string;
DEFINE FIELD field3 ON TABLE indexed_collection TYPE string;
DEFINE INDEX index_name ON TABLE indexed_collection FIELDS field1,field2;
DEFINE INDEX unique_index_name ON TABLE indexed_collection FIELDS field1,field2 UNIQUE;
DEFINE INDEX search_index_name ON TABLE indexed_collection FIELDS field3 SEARCH ANALYZER analyzer_name HIGHLIGHTS;[!NOTE]
only the used tokenizers (used in a configuration) will be collectedtable an field permissionsYou can define permissions on collections through the config :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel,ConfigDictfrompydantic_surql.typesimport(SurQLTableConfig,SurQLPermissions,)Metadata.clear()permission_config=SurQLTableConfig(permissions=SurQLPermissions(select=["WHERE published = true","OR user = $auth.id"],create=["WHERE user = $auth.id"],update=["WHERE user = $auth.id"],delete=["WHERE user = $auth.id","OR $auth.admin = true"]))@surql_collection("permission_collection",permission_config)classPermissionCollection(BaseModel):field1:strfield2:strfield3:strpublished:boolprint(Metadata.collect())this will generate the following SDL :DEFINE TABLE permission_collection SCHEMAFULL PERMISSIONS
    FOR SELECT
        WHERE published = true
        OR user = $auth.id
    FOR CREATE
        WHERE user = $auth.id
    FOR UPDATE
        WHERE user = $auth.id
    FOR DELETE
        WHERE user = $auth.id
        OR $auth.admin = true;
DEFINE FIELD field1 ON TABLE permission_collection TYPE string;
DEFINE FIELD field2 ON TABLE permission_collection TYPE string;
DEFINE FIELD field3 ON TABLE permission_collection TYPE string;
DEFINE FIELD published ON TABLE permission_collection TYPE bool;You can define field permissions throught theSurQLFieldConfigfunction.it's a wrapper around the pydanticFieldfunction (so you can use all the properties from theFielddefinition) :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel,ConfigDictfrompydantic_surql.typesimport(SurQLTableConfig,SurQLPermissions,SurQLFieldConfig)fields_permission=SurQLPermissions(select=["WHERE user = $auth.id"],create=["WHERE user = $auth.id"],update=["WHERE user = $auth.id"],delete=["WHERE user = $auth.id","OR $auth.admin = true"])@surql_collection("permission_collection",permission_config)classFieldPermissionsCollection(BaseModel):field1:str=SurQLFieldConfig(permissions=fields_permission,min_length=2)field2:strfield3:strpublished:boolprint(Metadata.collect())this will generate the following SDL:DEFINE FIELD field1 ON TABLE field_permission_collection TYPE string PERMISSIONS
    FOR SELECT
        WHERE user = $auth.id
    FOR CREATE
        WHERE user = $auth.id
    FOR UPDATE
        WHERE user = $auth.id
    FOR DELETE
        WHERE user = $auth.id
        OR $auth.admin = true;
DEFINE FIELD field2 ON TABLE field_permission_collection TYPE string;
DEFINE FIELD field3 ON TABLE field_permission_collection TYPE string;
DEFINE FIELD published ON TABLE field_permission_collection TYPE bool;events definitionsYou can define events through the collection config :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel,ConfigDictfrompydantic_surql.typesimportSurQLTableConfig,SurQLEventevent_config=SurQLTableConfig(events=[SurQLEvent(name="event_name",whenSDL=["$event =\"INSERT\"","$event =\"UPDATE\""],querySDL="INSERT INTO notification_collection (name, collection) VALUES ('something changed', 'event_collection')")])@surql_collection("event_collection",event_config)classEventCollection(BaseModel):field1:strfield2:strfield3:strprint(Metadata.collect())this will generate the following SDL:DEFINE ANALYZER analyzer_name TOKENIZERS blank;

DEFINE TABLE event_collection SCHEMAFULL;
DEFINE FIELD field1 ON TABLE event_collection TYPE string;
DEFINE FIELD field2 ON TABLE event_collection TYPE string;
DEFINE FIELD field3 ON TABLE event_collection TYPE string;
DEFINE EVENT event_name ON TABLE event_collection WHEN $event = "INSERT" OR $event = "UPDATE" THEN (INSERT INTO notification_collection (name, collection) VALUES ('something changed', 'event_collection'));Types definitionsbasic typesto define a basic type you can use the following python types :fromenumimportEnumfrompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModelfromdatetimeimportdatetimefromtypingimportAnyclassBasicEnum(Enum):ONE="ONE"TWO="TWO"THREE="THREE"FOUR=4FIVE=5SIX=6@surql_collection("basic_types")classBasicTypes(BaseModel):id:strstring:strnumber:intnumber_two:floatdate:datetimeflag:boolany_v:Anyenum_v:BasicEnumprint(Metadata.collect())this will generate the following SDL:DEFINE TABLE basic_types SCHEMAFULL;
DEFINE FIELD string ON TABLE basic_types TYPE string;
DEFINE FIELD number ON TABLE basic_types TYPE number;
DEFINE FIELD number_two ON TABLE basic_types TYPE number;
DEFINE FIELD date ON TABLE basic_types TYPE datetime;
DEFINE FIELD flag ON TABLE basic_types TYPE bool;
DEFINE FIELD any_v ON TABLE basic_types TYPE any;
DEFINE FIELD enum_v ON TABLE basic_types TYPE string|number ASSERT ($value in ["ONE","TWO","THREE",4,5,6]);union typesto define union types you can use theUnion[T, Y]or the|notation :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModelfromtypingimportUnionfromdatetimeimportdatetime@surql_collection("union_types")classUnionTypes(BaseModel):id:strstr_number:Union[str,int]date_timestamp:int|datetime#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE union_types SCHEMAFULL;
DEFINE FIELD str_number ON TABLE union_types TYPE string|number;
DEFINE FIELD date_timestamp ON TABLE union_types TYPE number|datetime;optional and null typesto define an optional type you can use theOptionalnotation :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModelfromtypingimportOptional@surql_collection("optional_types")classOptionalTypes(BaseModel):id:stropt_str:Optional[str]#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE optional_types SCHEMAFULL;
DEFINE FIELD opt_str ON TABLE optional_types TYPE option<string>;to define a null value you can use theSurQLNullabletype :frompydantic_surqlimportsurql_collection,Metadatafrompydantic_surql.typesimportSurQLNullablefrompydanticimportBaseModel@surql_collection("nullable_types")classBasicTypes(BaseModel):id:strnullable_str:str|SurQLNullable#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE nullable_types SCHEMAFULL;
DEFINE FIELD nullable_str ON TABLE nullable_types TYPE string|null;[!CAUTION]
usingNonewill result in anoptionalfield (Optional[T] <=> T | None)array typesto define array types you can use thelist[T]notation.You can nest arrays as much as you want :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel@surql_collection("array_types")classArrayTypes(BaseModel):id:strstr_list:list[str|int]list_str_list:list[list[str]]list_list_str_list:list[list[list[str]]]#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE array_types SCHEMAFULL;
DEFINE FIELD str_list ON TABLE array_types TYPE array;
DEFINE FIELD str_list.* ON TABLE array_types TYPE string|number;
DEFINE FIELD list_str_list ON TABLE array_types TYPE array;
DEFINE FIELD list_str_list.* ON TABLE array_types TYPE array;
DEFINE FIELD list_str_list.*.* ON TABLE array_types TYPE string;
DEFINE FIELD list_list_str_list ON TABLE array_types TYPE array;
DEFINE FIELD list_list_str_list.* ON TABLE array_types TYPE array;
DEFINE FIELD list_list_str_list.*.* ON TABLE array_types TYPE array;
DEFINE FIELD list_list_str_list.*.*.* ON TABLE array_types TYPE string;set typesto define set types you can use theset[T]notation.You can nest sets as much as you want :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel@surql_collection("set_types")classSetTypes(BaseModel):id:strstr_set:set[str|int]set_str_set:set[set[str]]set_set_str_set:set[set[set[str]]]#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE set_types SCHEMAFULL;
DEFINE FIELD str_set ON TABLE set_types TYPE set;
DEFINE FIELD str_set.* ON TABLE set_types TYPE string|number;
DEFINE FIELD set_str_set ON TABLE set_types TYPE set;
DEFINE FIELD set_str_set.* ON TABLE set_types TYPE set;
DEFINE FIELD set_str_set.*.* ON TABLE set_types TYPE string;
DEFINE FIELD set_set_str_set ON TABLE set_types TYPE set;
DEFINE FIELD set_set_str_set.* ON TABLE set_types TYPE set;
DEFINE FIELD set_set_str_set.*.* ON TABLE set_types TYPE set;
DEFINE FIELD set_set_str_set.*.*.* ON TABLE set_types TYPE string;object typesto define an object you can use a Pydantic model.to mark the object asflexible, you can usepydantic built in featureYou can also nest objects as much as you want :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel,ConfigDictclassSubSubObject(BaseModel):model_config=ConfigDict(extra='allow')some_mandatory_field:str#...classSubObject(BaseModel):sub_sub_object:SubSubObject#...@surql_collection("object_types")classObjectTypes(BaseModel):id:strsub_object:SubObject#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE object_types SCHEMAFULL;
DEFINE FIELD sub_object ON TABLE object_types TYPE object;
DEFINE FIELD sub_object.sub_sub_object ON TABLE object_types FLEXIBLE TYPE object;
DEFINE FIELD sub_object.sub_sub_object.some_mandatory_field ON TABLE object_types TYPE string;[!WARNING]
surql doesn't support recursive objects, if you want to use recursive structures use arecorddefinitionrecord typesInternally the@surql_collectiondecorator will mark the model as a surql collection.Defining a record is simple as :frompydantic_surqlimportsurql_collection,MetadatafrompydanticimportBaseModel@surql_collection("record_target")classRecordTarget(BaseModel):id:strsome_field:str#...@surql_collection("record_types")classRecordTypes(BaseModel):id:strrecord_target:RecordTarget#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE record_target SCHEMAFULL;
DEFINE FIELD some_field ON TABLE record_target TYPE string;

DEFINE TABLE record_types SCHEMAFULL;
DEFINE FIELD record_target ON TABLE record_types TYPE record<record_target>;It's also possible to use a generic record :frompydantic_surqlimportsurql_collection,Metadatafrompydantic_surql.typesimportSurQLAnyRecordfrompydanticimportBaseModel@surql_collection("generic_record_types")classGenericRecordTypes(BaseModel):id:strrecord_target:SurQLAnyRecord#...print(Metadata.collect())this will generate the following SDL:DEFINE TABLE generic_record_types SCHEMAFULL;
DEFINE FIELD id ON TABLE generic_record_types TYPE string;
DEFINE FIELD record_target ON TABLE generic_record_types TYPE record();[!NOTE]SurQLAnyRecord <=> Type[dict]so your pydantic model wont be able to map to pydantic classes automatically.