12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010 |
- import type {
- AttributeNames,
- Attributes,
- Cast,
- Col,
- Fn,
- InferAttributes,
- Literal,
- Range,
- WhereOperators,
- WhereOptions,
- } from '@sequelize/core';
- import { DataTypes, JSON_NULL, Model, Op, SQL_NULL, and, json, or, sql } from '@sequelize/core';
- import type { FormatWhereOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js';
- import { expect } from 'chai';
- import { expectTypeOf } from 'expect-type';
- import attempt from 'lodash/attempt';
- import util from 'node:util';
- import { createTester, expectsql, getTestDialectTeaser, sequelize } from '../../support';
- const { literal, col, where, fn, cast, attribute } = sql;
- const queryGen = sequelize.dialect.queryGenerator;
- // Notice: [] will be replaced by dialect specific tick/quote character
- // when there is no dialect specific expectation but only a default expectation
- // TODO: fix and resolve any .skip test
- type Expectations = {
- [dialectName: string]: string | Error;
- };
- const dialectSupportsBigInt = () => sequelize.dialect.supports.dataTypes.BIGINT;
- const dialectSupportsArray = () => sequelize.dialect.supports.dataTypes.ARRAY;
- const dialectSupportsRange = () => sequelize.dialect.supports.dataTypes.RANGE;
- const dialectSupportsJsonB = () => sequelize.dialect.supports.dataTypes.JSONB;
- const dialectSupportsJson = () => sequelize.dialect.supports.dataTypes.JSON;
- const dialectSupportsJsonOperations = () => sequelize.dialect.supports.jsonOperations;
- const dialectSupportsJsonQuotedExtraction = () => sequelize.dialect.supports.jsonExtraction.quoted;
- const dialectSupportsJsonUnquotedExtraction = () =>
- sequelize.dialect.supports.jsonExtraction.unquoted;
- interface SomeInterface {
- foo: string;
- }
- class TestModel extends Model<InferAttributes<TestModel>> {
- declare intAttr1: number;
- declare intAttr2: number;
- declare nullableIntAttr: number | null;
- declare intArrayAttr: number[];
- declare intRangeAttr: Range<number>;
- declare dateRangeAttr: Range<Date>;
- declare stringAttr: string;
- declare binaryAttr: Buffer;
- declare dateAttr: Date;
- declare booleanAttr: boolean;
- declare bigIntAttr: bigint;
- declare jsonAttr: object | null;
- declare jsonbAttr: object | null;
- declare aliasedInt: number;
- declare aliasedJsonAttr: object;
- declare aliasedJsonbAttr: object;
- declare jsonbTypeLiteralAttr: { foo: string };
- declare jsonbInterfaceAttr: SomeInterface;
- declare uuidAttr: string;
- }
- type TestModelWhere = WhereOptions<Attributes<TestModel>>;
- describe(getTestDialectTeaser('SQL'), () => {
- before(() => {
- TestModel.init(
- {
- intAttr1: DataTypes.INTEGER,
- intAttr2: DataTypes.INTEGER,
- nullableIntAttr: DataTypes.INTEGER,
- ...(dialectSupportsArray() && {
- intArrayAttr: DataTypes.ARRAY(DataTypes.INTEGER),
- intRangeAttr: DataTypes.RANGE(DataTypes.INTEGER),
- dateRangeAttr: DataTypes.RANGE(DataTypes.DATE(3)),
- }),
- stringAttr: DataTypes.STRING,
- binaryAttr: DataTypes.BLOB,
- dateAttr: DataTypes.DATE(3),
- booleanAttr: DataTypes.BOOLEAN,
- ...(dialectSupportsBigInt() && { bigIntAttr: DataTypes.BIGINT }),
- aliasedInt: { type: DataTypes.INTEGER, field: 'aliased_int' },
- ...(dialectSupportsJson() && {
- jsonAttr: { type: DataTypes.JSON },
- aliasedJsonAttr: { type: DataTypes.JSON, field: 'aliased_json' },
- }),
- ...(dialectSupportsJsonB() && {
- jsonbAttr: { type: DataTypes.JSONB },
- aliasedJsonbAttr: { type: DataTypes.JSONB, field: 'aliased_jsonb' },
- jsonbTypeLiteralAttr: { type: DataTypes.JSONB },
- jsonbInterfaceAttr: { type: DataTypes.JSONB },
- }),
- uuidAttr: DataTypes.UUID,
- },
- { sequelize },
- );
- });
- describe('whereQuery', () => {
- it('prefixes its output with WHERE when it is not empty', () => {
- expectsql(queryGen.whereQuery({ firstName: 'abc' }), {
- default: `WHERE [firstName] = 'abc'`,
- mssql: `WHERE [firstName] = N'abc'`,
- });
- });
- it('returns an empty string if the input results in an empty query', () => {
- expectsql(queryGen.whereQuery({ firstName: { [Op.notIn]: [] } }), {
- default: '',
- });
- });
- });
- describe('whereItemsQuery', () => {
- type IncludesType<Haystack, Needle> = Needle extends any
- ? Extract<Haystack, Needle> extends never
- ? false
- : true
- : never;
- /**
- * 'OperatorsSupportingSequelizeValueMethods' lists all operators
- * that accept values: `col()`, `literal()`, `fn()`, `cast()`, and { [Op.col] }
- */
- type OperatorsSupportingSequelizeValueMethods = keyof {
- [Key in keyof WhereOperators<number> as IncludesType<
- WhereOperators<number>[Key],
- Col | Literal | Fn | Cast | { [Op.col]: string }
- > extends true
- ? Key
- : never]: WhereOperators<number>[Key];
- };
- /**
- * Tests whether an operator is compatible with the 5 sequelize methods that can be used as values:
- * - col()
- * - literal()
- * - fn()
- * - cast()
- * - legacy Op.col
- *
- * If there is a typescript error on the operator passed to this function, then
- * the typings in {@link WhereOperators} for the provided operator are incorrect.
- *
- * @param operator
- * @param sqlOperator
- */
- function testSequelizeValueMethods(
- operator: OperatorsSupportingSequelizeValueMethods,
- sqlOperator: string,
- ): void {
- testSql(
- { intAttr1: { [operator]: { [Op.col]: 'intAttr2' } } },
- {
- default: `[intAttr1] ${sqlOperator} [intAttr2]`,
- },
- );
- testSql(
- { intAttr1: { [operator]: col('intAttr2') } },
- {
- default: `[intAttr1] ${sqlOperator} [intAttr2]`,
- },
- );
- testSql(
- { intAttr1: { [operator]: literal('literal') } },
- {
- default: `[intAttr1] ${sqlOperator} literal`,
- },
- );
- testSql(
- { intAttr1: { [operator]: fn('NOW') } },
- {
- default: `[intAttr1] ${sqlOperator} NOW()`,
- },
- );
- testSql(
- { intAttr1: { [operator]: fn('SUM', { [Op.col]: 'intAttr2' }) } },
- {
- default: `[intAttr1] ${sqlOperator} SUM([intAttr2])`,
- },
- );
- testSql(
- { intAttr1: { [operator]: cast(col('intAttr2'), 'string') } },
- {
- default: `[intAttr1] ${sqlOperator} CAST([intAttr2] AS STRING)`,
- },
- );
- testSql(
- { intAttr1: { [operator]: cast({ [Op.col]: 'intAttr2' }, 'string') } },
- {
- default: `[intAttr1] ${sqlOperator} CAST([intAttr2] AS STRING)`,
- },
- );
- testSql(
- { intAttr1: { [operator]: cast(12, 'string') } },
- {
- default: `[intAttr1] ${sqlOperator} CAST(12 AS STRING)`,
- },
- );
- }
- /**
- * 'OperatorsSupportingSequelizeValueMethods' lists all operators
- * that accept values: `col()`, `literal()`, `fn()`, `cast()`, and { [Op.col] }
- */
- type OperatorsSupportingAnyAll<AttributeType> = keyof {
- [Key in keyof WhereOperators<AttributeType> as IncludesType<
- WhereOperators<AttributeType>[Key],
- | { [Op.all]: any[] | Literal | { [Op.values]: any[] } }
- | { [Op.any]: any[] | Literal | { [Op.values]: any[] } }
- > extends true
- ? Key
- : never]: WhereOperators<AttributeType>[Key];
- };
- /**
- * Tests whether an operator is compatible with:
- * - Op.any (+ Op.values)
- * - Op.all (+ Op.values)
- *
- * If there is a typescript error on the operator passed to this function, then
- * the typings in {@link WhereOperators} for the provided operator are incorrect.
- *
- * @param operator
- * @param sqlOperator
- * @param testWithValues
- * @param attributeName
- */
- function testSupportsAnyAll<TestWithValue>(
- operator: OperatorsSupportingAnyAll<TestWithValue>,
- sqlOperator: string,
- testWithValues: TestWithValue[],
- attributeName: AttributeNames<TestModel> = 'intAttr1',
- ) {
- if (!dialectSupportsArray()) {
- return;
- }
- const arrayOperators: Array<[jsOp: symbol, sqlOp: string]> = [
- [Op.any, 'ANY'],
- [Op.all, 'ALL'],
- ];
- for (const [arrayOperator, arraySqlOperator] of arrayOperators) {
- testSql(
- { [attributeName]: { [operator]: { [arrayOperator]: testWithValues } } },
- {
- default: `[${attributeName}] ${sqlOperator} ${arraySqlOperator} (ARRAY[${testWithValues.map(v => util.inspect(v)).join(',')}])`,
- postgres: `"${attributeName}" ${sqlOperator} ${arraySqlOperator} (ARRAY[${testWithValues.map(v => util.inspect(v)).join(',')}]${attributeName === 'stringAttr' ? '::VARCHAR(255)[]' : ''})`,
- },
- );
- testSql(
- { [attributeName]: { [operator]: { [arrayOperator]: literal('literal') } } },
- {
- default: `[${attributeName}] ${sqlOperator} ${arraySqlOperator} (literal)`,
- },
- );
- // e.g. "col" LIKE ANY (VALUES ("col2"))
- testSql(
- {
- [attributeName]: {
- [operator]: {
- [arrayOperator]: {
- [Op.values]: [
- literal('literal'),
- fn('UPPER', col('col2')),
- col('col3'),
- cast(col('col'), 'string'),
- testWithValues[0],
- ],
- },
- },
- },
- },
- {
- default: `[${attributeName}] ${sqlOperator} ${arraySqlOperator} (VALUES (literal), (UPPER("col2")), ("col3"), (CAST("col" AS STRING)), (${util.inspect(testWithValues[0])}))`,
- },
- );
- }
- }
- const testSql = createTester(
- (it, whereObj: TestModelWhere, expectations: Expectations, options?: FormatWhereOptions) => {
- it(
- util.inspect(whereObj, { depth: 10 }) + (options ? `, ${util.inspect(options)}` : ''),
- () => {
- const sqlOrError = attempt(() =>
- queryGen.whereItemsQuery(whereObj, {
- ...options,
- model: TestModel,
- }),
- );
- return expectsql(sqlOrError, expectations);
- },
- );
- },
- );
- // "where" is typically optional. If the user sets it to undefined, we treat is as if the option was not set.
- testSql(undefined, {
- default: '',
- });
- testSql(
- {},
- {
- default: '',
- },
- );
- testSql([], {
- default: '',
- });
- // @ts-expect-error -- not supported, testing that it throws
- testSql(null, {
- default:
- new Error(`Invalid value received for the "where" option. Refer to the sequelize documentation to learn which values the "where" option accepts.
- Value: null
- Caused by: Invalid Query: expected a plain object, an array or a sequelize SQL method but got null`),
- });
- // @ts-expect-error -- not supported, testing that it throws
- testSql(10, {
- default:
- new Error(`Invalid value received for the "where" option. Refer to the sequelize documentation to learn which values the "where" option accepts.
- Value: 10
- Caused by: Invalid Query: expected a plain object, an array or a sequelize SQL method but got 10`),
- });
- testSql(
- { intAttr1: undefined },
- {
- default:
- new Error(`Invalid value received for the "where" option. Refer to the sequelize documentation to learn which values the "where" option accepts.
- Value: { intAttr1: undefined }
- Caused by: "undefined" cannot be escaped`),
- },
- );
- testSql(
- // @ts-expect-error -- user does not exist
- { intAttr1: 1, user: undefined },
- { default: new Error('"undefined" cannot be escaped') },
- );
- testSql(
- { intAttr1: 1 },
- {
- default: '[User].[intAttr1] = 1',
- },
- { mainAlias: 'User' },
- );
- testSql(
- { dateAttr: { $gte: '2022-11-06' } },
- { default: new Error(`{ '$gte': '2022-11-06' } is not a valid date`) },
- );
- testSql(literal('raw sql'), {
- default: 'raw sql',
- });
- describe('value serialization', () => {
- // string
- testSql(
- { stringAttr: '1' },
- {
- default: `[stringAttr] = '1'`,
- mssql: `[stringAttr] = N'1'`,
- },
- );
- testSql(
- {
- stringAttr: 'here is a null char: \0',
- },
- {
- default: "[stringAttr] = 'here is a null char: \\0'",
- snowflake: '"stringAttr" = \'here is a null char: \0\'',
- mssql: "[stringAttr] = N'here is a null char: \0'",
- db2: '"stringAttr" = \'here is a null char: \0\'',
- ibmi: '"stringAttr" = \'here is a null char: \0\'',
- sqlite3: "`stringAttr` = 'here is a null char: \0'",
- },
- );
- testSql(
- {
- dateAttr: 1_356_998_400_000,
- },
- {
- default: `[dateAttr] = '2013-01-01 00:00:00.000 +00:00'`,
- 'mariadb mysql': `\`dateAttr\` = '2013-01-01 00:00:00.000'`,
- mssql: `[dateAttr] = N'2013-01-01 00:00:00.000 +00:00'`,
- 'db2 snowflake ibmi': `"dateAttr" = '2013-01-01 00:00:00.000'`,
- },
- );
- describe('Buffer', () => {
- testSql(
- { binaryAttr: Buffer.from('Sequelize') },
- {
- ibmi: `"binaryAttr" = BLOB(X'53657175656c697a65')`,
- postgres: `"binaryAttr" = '\\x53657175656c697a65'`,
- 'sqlite3 mariadb mysql': "`binaryAttr` = X'53657175656c697a65'",
- db2: `"binaryAttr" = BLOB('Sequelize')`,
- snowflake: `"binaryAttr" = X'53657175656c697a65'`,
- mssql: '[binaryAttr] = 0x53657175656c697a65',
- },
- );
- // Including a quote (') to ensure dialects that don't convert to hex are safe from SQL injection.
- testSql(
- { binaryAttr: [Buffer.from(`Seque'lize1`), Buffer.from('Sequelize2')] },
- {
- ibmi: `"binaryAttr" IN (BLOB(X'5365717565276c697a6531'), BLOB(X'53657175656c697a6532'))`,
- postgres: `"binaryAttr" IN ('\\x5365717565276c697a6531', '\\x53657175656c697a6532')`,
- 'sqlite3 mariadb mysql':
- "`binaryAttr` IN (X'5365717565276c697a6531', X'53657175656c697a6532')",
- db2: `"binaryAttr" IN (BLOB('Seque''lize1'), BLOB('Sequelize2'))`,
- snowflake: `"binaryAttr" IN (X'5365717565276c697a6531', X'53657175656c697a6532')`,
- mssql: '[binaryAttr] IN (0x5365717565276c697a6531, 0x53657175656c697a6532)',
- },
- );
- });
- });
- describe('implicit operator', () => {
- testSql(
- { intAttr1: 1 },
- {
- default: '[intAttr1] = 1',
- },
- );
- testSql(
- { stringAttr: '1' },
- {
- default: `[stringAttr] = '1'`,
- mssql: `[stringAttr] = N'1'`,
- },
- );
- testSql(
- { intAttr1: [1, 2] },
- {
- default: '[intAttr1] IN (1, 2)',
- },
- );
- testSql(
- { stringAttr: ['1', '2'] },
- {
- default: `[stringAttr] IN ('1', '2')`,
- mssql: `[stringAttr] IN (N'1', N'2')`,
- },
- );
- testSql(
- { intAttr1: ['not-an-int'] },
- { default: new Error(`'not-an-int' is not a valid integer`) },
- );
- testSql(
- { 'stringAttr::integer': 1 },
- {
- default: 'CAST([stringAttr] AS INTEGER) = 1',
- },
- );
- testSql(
- { $intAttr1$: 1 },
- {
- default: '[intAttr1] = 1',
- },
- );
- testSql(
- { '$stringAttr$::integer': 1 },
- {
- default: 'CAST([stringAttr] AS INTEGER) = 1',
- },
- );
- testSql(
- { '$association.attribute$': 1 },
- {
- default: '[association].[attribute] = 1',
- },
- );
- testSql(
- { '$association.attribute$::integer': 1 },
- {
- default: 'CAST([association].[attribute] AS INTEGER) = 1',
- },
- );
- testSql(
- { booleanAttr: true },
- {
- default: `[booleanAttr] = true`,
- mssql: '[booleanAttr] = 1',
- sqlite3: '`booleanAttr` = 1',
- ibmi: '"booleanAttr" = 1',
- },
- );
- testSql(
- {
- stringAttr: 'a project',
- intAttr1: {
- [Op.or]: [[1, 2, 3], { [Op.gt]: 10 }],
- },
- },
- {
- default: "[stringAttr] = 'a project' AND ([intAttr1] IN (1, 2, 3) OR [intAttr1] > 10)",
- mssql: "[stringAttr] = N'a project' AND ([intAttr1] IN (1, 2, 3) OR [intAttr1] > 10)",
- },
- );
- testSql(
- { nullableIntAttr: null },
- {
- default: '[nullableIntAttr] IS NULL',
- },
- );
- testSql(
- { nullableIntAttr: SQL_NULL },
- {
- default: '[nullableIntAttr] IS NULL',
- },
- );
- testSql(
- { dateAttr: new Date('2021-01-01T00:00:00Z') },
- {
- default: `[dateAttr] = '2021-01-01 00:00:00.000 +00:00'`,
- mssql: `[dateAttr] = N'2021-01-01 00:00:00.000 +00:00'`,
- 'mariadb mysql': `\`dateAttr\` = '2021-01-01 00:00:00.000'`,
- 'db2 ibmi snowflake': `"dateAttr" = '2021-01-01 00:00:00.000'`,
- },
- );
- testSql(
- { intAttr1: { [Op.col]: 'intAttr2' } },
- {
- default: '[intAttr1] = [intAttr2]',
- },
- );
- testSql(
- { intAttr1: col('intAttr2') },
- {
- default: '[intAttr1] = [intAttr2]',
- },
- );
- testSql(
- { intAttr1: literal('literal') },
- {
- default: '[intAttr1] = literal',
- },
- );
- testSql(
- { stringAttr: fn('UPPER', col('stringAttr')) },
- {
- default: '[stringAttr] = UPPER([stringAttr])',
- },
- );
- testSql(
- { stringAttr: fn('UPPER', { [Op.col]: 'stringAttr' }) },
- {
- default: '[stringAttr] = UPPER([stringAttr])',
- },
- );
- testSql(
- { stringAttr: cast(col('intAttr1'), 'string') },
- {
- default: '[stringAttr] = CAST([intAttr1] AS STRING)',
- },
- );
- testSql(
- { stringAttr: cast({ [Op.col]: 'intAttr1' }, 'string') },
- {
- default: '[stringAttr] = CAST([intAttr1] AS STRING)',
- },
- );
- testSql(
- { stringAttr: cast('abc', 'string') },
- {
- default: `[stringAttr] = CAST('abc' AS STRING)`,
- mssql: `[stringAttr] = CAST(N'abc' AS STRING)`,
- },
- );
- if (dialectSupportsArray()) {
- testSql(
- { intArrayAttr: [1, 2] },
- {
- default: `[intArrayAttr] = ARRAY[1,2]`,
- },
- );
- testSql(
- { intArrayAttr: [] },
- {
- default: `[intArrayAttr] = ARRAY[]::INTEGER[]`,
- },
- );
- // when using arrays, Op.in is never included
- testSql(
- // @ts-expect-error -- Omitting the operator with an array attribute is always Op.eq, never Op.in
- { intArrayAttr: [[1, 2]] },
- { default: new Error('[ 1, 2 ] is not a valid integer') },
- );
- testSql(
- { intAttr1: { [Op.any]: [2, 3, 4] } },
- {
- default: '[intAttr1] = ANY (ARRAY[2,3,4])',
- },
- );
- testSql(
- { intAttr1: { [Op.any]: literal('literal') } },
- {
- default: '[intAttr1] = ANY (literal)',
- },
- );
- testSql(
- { intAttr1: { [Op.any]: { [Op.values]: [col('col')] } } },
- {
- default: '[intAttr1] = ANY (VALUES ([col]))',
- },
- );
- testSql(
- { intAttr1: { [Op.all]: [2, 3, 4] } },
- {
- default: '[intAttr1] = ALL (ARRAY[2,3,4])',
- },
- );
- testSql(
- { intAttr1: { [Op.all]: literal('literal') } },
- {
- default: '[intAttr1] = ALL (literal)',
- },
- );
- testSql(
- { intAttr1: { [Op.all]: { [Op.values]: [col('col')] } } },
- {
- default: '[intAttr1] = ALL (VALUES ([col]))',
- },
- );
- // e.g. "col" LIKE ANY (VALUES ("col2"))
- testSql(
- {
- intAttr1: {
- [Op.any]: {
- [Op.values]: [
- literal('literal'),
- fn('UPPER', col('col2')),
- col('col3'),
- cast(col('col'), 'string'),
- 1,
- ],
- },
- },
- },
- {
- default: `[intAttr1] = ANY (VALUES (literal), (UPPER([col2])), ([col3]), (CAST([col] AS STRING)), (1))`,
- },
- );
- }
- });
- describe('Op.eq', () => {
- testSql(
- { intAttr1: { [Op.eq]: 1 } },
- {
- default: '[intAttr1] = 1',
- },
- );
- testSql(
- { 'intAttr1::integer': { [Op.eq]: 1 } },
- {
- default: 'CAST([intAttr1] AS INTEGER) = 1',
- },
- );
- testSql(
- { $intAttr1$: { [Op.eq]: 1 } },
- {
- default: '[intAttr1] = 1',
- },
- );
- testSql(
- { '$intAttr1$::integer': { [Op.eq]: 1 } },
- {
- default: 'CAST([intAttr1] AS INTEGER) = 1',
- },
- );
- testSql(
- { '$association.attribute$': { [Op.eq]: 1 } },
- {
- default: '[association].[attribute] = 1',
- },
- );
- testSql(
- { '$association.attribute$::integer': { [Op.eq]: 1 } },
- {
- default: `CAST([association].[attribute] AS INTEGER) = 1`,
- },
- );
- if (dialectSupportsArray()) {
- // @ts-expect-error -- intArrayAttr is not an array
- const ignore: TestModelWhere = { intAttr1: { [Op.eq]: [1, 2] } };
- testSql(
- { intArrayAttr: { [Op.eq]: [1, 2] } },
- {
- default: '[intArrayAttr] = ARRAY[1,2]',
- },
- );
- }
- {
- // @ts-expect-error -- intAttr1 is not nullable
- const ignore: TestModelWhere = { intAttr1: { [Op.eq]: null } };
- // this one is
- testSql(
- { nullableIntAttr: { [Op.eq]: null } },
- {
- default: '[nullableIntAttr] IS NULL',
- },
- );
- }
- testSql(
- { booleanAttr: { [Op.eq]: true } },
- {
- default: '[booleanAttr] = true',
- 'mssql sqlite3 ibmi': '[booleanAttr] = 1',
- },
- );
- testSequelizeValueMethods(Op.eq, '=');
- testSupportsAnyAll(Op.eq, '=', [2, 3, 4]);
- });
- describe('Op.ne', () => {
- testSql(
- { intAttr1: { [Op.ne]: 1 } },
- {
- default: '[intAttr1] != 1',
- },
- );
- if (dialectSupportsArray()) {
- testSql(
- { intArrayAttr: { [Op.ne]: [1, 2] } },
- {
- default: '[intArrayAttr] != ARRAY[1,2]',
- },
- );
- }
- testSql(
- { nullableIntAttr: { [Op.ne]: null } },
- {
- default: '[nullableIntAttr] IS NOT NULL',
- },
- );
- testSql(
- { booleanAttr: { [Op.ne]: true } },
- {
- default: '[booleanAttr] != true',
- 'mssql ibmi sqlite3': '[booleanAttr] != 1',
- },
- );
- testSequelizeValueMethods(Op.ne, '!=');
- testSupportsAnyAll(Op.ne, '!=', [2, 3, 4]);
- });
- describe('Op.is', () => {
- {
- // @ts-expect-error -- intAttr is not nullable
- const ignore: TestModelWhere = { intAttr: { [Op.is]: null } };
- }
- {
- // @ts-expect-error -- stringAttr is not a boolean
- const ignore: TestModelWhere = { stringAttr: { [Op.is]: true } };
- }
- testSql(
- { nullableIntAttr: { [Op.is]: null } },
- {
- default: '[nullableIntAttr] IS NULL',
- },
- );
- testSql(
- { nullableIntAttr: { [Op.is]: SQL_NULL } },
- {
- default: '[nullableIntAttr] IS NULL',
- },
- );
- testSql(
- { booleanAttr: { [Op.is]: false } },
- {
- default: '[booleanAttr] IS false',
- 'mssql ibmi sqlite3': '[booleanAttr] IS 0',
- },
- );
- testSql(
- { booleanAttr: { [Op.is]: true } },
- {
- default: '[booleanAttr] IS true',
- 'mssql ibmi sqlite3': '[booleanAttr] IS 1',
- },
- );
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: 1 } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: { [Op.col]: 'intAttr2' } } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: col('intAttr2') } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- testSql(
- { intAttr1: { [Op.is]: literal('UNKNOWN') } },
- {
- default: '[intAttr1] IS UNKNOWN',
- },
- );
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: fn('UPPER', col('intAttr2')) } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: cast(col('intAttr2'), 'boolean') } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- if (dialectSupportsArray()) {
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: { [Op.any]: [2, 3] } } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- testSql(
- // @ts-expect-error -- not supported, testing that it throws
- { intAttr1: { [Op.is]: { [Op.all]: [2, 3, 4] } } },
- {
- default: new Error(
- 'Operators Op.is and Op.isNot can only be used with null, true, false or a literal.',
- ),
- },
- );
- }
- });
- describe('Op.isNot', () => {
- testSql(
- { nullableIntAttr: { [Op.isNot]: null } },
- {
- default: '[nullableIntAttr] IS NOT NULL',
- },
- );
- testSql(
- { booleanAttr: { [Op.isNot]: false } },
- {
- default: '[booleanAttr] IS NOT false',
- 'mssql ibmi sqlite3': '[booleanAttr] IS NOT 0',
- },
- );
- testSql(
- { booleanAttr: { [Op.isNot]: true } },
- {
- default: '[booleanAttr] IS NOT true',
- 'mssql ibmi sqlite3': '[booleanAttr] IS NOT 1',
- },
- );
- });
- describe('Op.not', () => {
- testSql(
- { [Op.not]: {} },
- {
- default: '',
- },
- );
- testSql(
- {
- [Op.not]: {
- [Op.not]: {},
- },
- },
- {
- default: '',
- },
- );
- testSql(
- { [Op.not]: [] },
- {
- default: '',
- },
- );
- testSql(
- { nullableIntAttr: { [Op.not]: {} } },
- {
- default: '',
- },
- );
- testSql(
- { nullableIntAttr: { [Op.not]: null } },
- {
- default: 'NOT ([nullableIntAttr] IS NULL)',
- },
- );
- testSql(
- { booleanAttr: { [Op.not]: false } },
- {
- default: 'NOT ([booleanAttr] = false)',
- mssql: 'NOT ([booleanAttr] = 0)',
- ibmi: 'NOT ("booleanAttr" = 0)',
- sqlite3: 'NOT (`booleanAttr` = 0)',
- },
- );
- testSql(
- { booleanAttr: { [Op.not]: true } },
- {
- default: 'NOT ([booleanAttr] = true)',
- mssql: 'NOT ([booleanAttr] = 1)',
- ibmi: 'NOT ("booleanAttr" = 1)',
- sqlite3: 'NOT (`booleanAttr` = 1)',
- },
- );
- testSql(
- { intAttr1: { [Op.not]: 1 } },
- {
- default: 'NOT ([intAttr1] = 1)',
- },
- );
- testSql(
- { intAttr1: { [Op.not]: [1, 2] } },
- {
- default: 'NOT ([intAttr1] IN (1, 2))',
- },
- );
- {
- // @ts-expect-error -- not a valid query: attribute does not exist.
- const ignore: TestModelWhere = { [Op.not]: { doesNotExist: 5 } };
- }
- testSql(
- { [Op.not]: { intAttr1: 5 } },
- {
- default: 'NOT ([intAttr1] = 5)',
- },
- );
- testSql(
- { [Op.not]: { intAttr1: { [Op.gt]: 5 } } },
- {
- default: 'NOT ([intAttr1] > 5)',
- },
- );
- testSql(
- { [Op.not]: where(col('intAttr1'), Op.eq, '5') },
- {
- default: `NOT ([intAttr1] = '5')`,
- mssql: `NOT ([intAttr1] = N'5')`,
- },
- );
- if (dialectSupportsJsonOperations() && dialectSupportsJsonQuotedExtraction()) {
- testSql(
- { [Op.not]: json('data.key', 10) },
- {
- postgres: `NOT ("data"->'key' = '10')`,
- sqlite3: `NOT (json_extract(\`data\`,'$.key') = '10')`,
- mariadb: `NOT (json_compact(json_extract(\`data\`,'$.key')) = '10')`,
- mysql: `NOT (json_extract(\`data\`,'$.key') = CAST('10' AS JSON))`,
- },
- );
- }
- testSql(
- { intAttr1: { [Op.not]: { [Op.gt]: 5 } } },
- {
- default: 'NOT ([intAttr1] > 5)',
- },
- );
- });
- function describeComparisonSuite(
- operator: typeof Op.gt | typeof Op.gte | typeof Op.lt | typeof Op.lte,
- sqlOperator: string,
- ) {
- // ensure gte, gt, lte, lt support the same typings, so we only have to test their typings once.
- // unfortunately, at time of writing (TS 4.5.5), TypeScript
- // does not detect an error in `{ [operator]: null }`
- // but it does detect an error in { [Op.gt]: null }`
- expectTypeOf<WhereOperators[typeof Op.gte]>().toEqualTypeOf<WhereOperators[typeof Op.gt]>();
- expectTypeOf<WhereOperators[typeof Op.lt]>().toEqualTypeOf<WhereOperators[typeof Op.gt]>();
- expectTypeOf<WhereOperators[typeof Op.lte]>().toEqualTypeOf<WhereOperators[typeof Op.gt]>();
- describe(`Op.${operator.description}`, () => {
- {
- const ignore: TestModelWhere = { intAttr1: { [Op.gt]: 1 } };
- testSql(
- { intAttr1: { [operator]: 1 } },
- {
- default: `[intAttr1] ${sqlOperator} 1`,
- },
- );
- }
- {
- const ignore: TestModelWhere = { stringAttr: { [Op.gt]: 'abc' } };
- testSql(
- { stringAttr: { [operator]: 'abc' } },
- {
- default: `[stringAttr] ${sqlOperator} 'abc'`,
- mssql: `[stringAttr] ${sqlOperator} N'abc'`,
- },
- );
- }
- if (dialectSupportsArray()) {
- const ignore: TestModelWhere = { intArrayAttr: { [Op.gt]: [1, 2] } };
- testSql(
- { intArrayAttr: { [operator]: [1, 2] } },
- {
- default: `[intArrayAttr] ${sqlOperator} ARRAY[1,2]`,
- },
- );
- }
- expectTypeOf({ intAttr1: { [Op.gt]: null } }).not.toMatchTypeOf<WhereOperators>();
- testSql(
- { intAttr1: { [operator]: null } },
- {
- default: `[intAttr1] ${sqlOperator} NULL`,
- },
- );
- testSequelizeValueMethods(operator, sqlOperator);
- testSupportsAnyAll(operator, sqlOperator, [2, 3, 4]);
- });
- }
- describeComparisonSuite(Op.gt, '>');
- describeComparisonSuite(Op.gte, '>=');
- describeComparisonSuite(Op.lt, '<');
- describeComparisonSuite(Op.lte, '<=');
- function describeBetweenSuite(
- operator: typeof Op.between | typeof Op.notBetween,
- sqlOperator: string,
- ) {
- // ensure between and notBetween support the same typings, so we only have to test their typings once.
- // unfortunately, at time of writing (TS 4.5.5), TypeScript
- // does not detect an error in `{ [operator]: null }`
- // but it does detect an error in { [Op.gt]: null }`
- expectTypeOf<WhereOperators[typeof Op.between]>().toEqualTypeOf<
- WhereOperators[typeof Op.notBetween]
- >();
- describe(`Op.${operator.description}`, () => {
- expectTypeOf({ id: { [Op.between]: [1, 2] } }).toMatchTypeOf<TestModelWhere>();
- expectTypeOf({
- id: { [Op.between]: [new Date(), new Date()] },
- }).toMatchTypeOf<TestModelWhere>();
- expectTypeOf({ id: { [Op.between]: ['a', 'b'] } }).toMatchTypeOf<TestModelWhere>();
- // expectTypeOf doesn't work with this one:
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: [1, 2] },
- };
- testSql(
- { intAttr1: { [operator]: [1, 2] } },
- {
- default: `[intAttr1] ${sqlOperator} 1 AND 2`,
- },
- );
- // @ts-expect-error -- must pass exactly 2 items
- const ignoreWrong: TestModelWhere = { intAttr1: { [Op.between]: [1, 2, 3] } };
- // @ts-expect-error -- must pass exactly 2 items
- const ignoreWrong2: TestModelWhere = { intAttr1: { [Op.between]: [1] } };
- testSql(
- { intAttr1: { [operator]: [1] } },
- {
- default: new Error(
- 'Operators Op.between and Op.notBetween must be used with an array of two values, or a literal.',
- ),
- },
- );
- // @ts-expect-error -- must pass exactly 2 items
- const ignoreWrong3: TestModelWhere = { intAttr1: { [Op.between]: [] } };
- }
- if (dialectSupportsArray()) {
- {
- const ignoreRight: TestModelWhere = {
- intArrayAttr: {
- [Op.between]: [
- [1, 2],
- [3, 4],
- ],
- },
- };
- testSql(
- {
- intArrayAttr: {
- [operator]: [
- [1, 2],
- [3, 4],
- ],
- },
- },
- {
- default: `[intArrayAttr] ${sqlOperator} ARRAY[1,2] AND ARRAY[3,4]`,
- },
- );
- }
- {
- // @ts-expect-error -- this is not valid because intAttr1 is not an array and cannot be compared to arrays
- const ignore: TestModelWhere = {
- intAttr1: {
- [Op.between]: [
- [1, 2],
- [3, 4],
- ],
- },
- };
- }
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: [col('col1'), col('col2')] },
- };
- testSql(
- { intAttr1: { [operator]: [col('col1'), col('col2')] } },
- {
- default: `[intAttr1] ${sqlOperator} [col1] AND [col2]`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: [literal('literal1'), literal('literal2')] },
- };
- testSql(
- { intAttr1: { [operator]: [literal('literal1'), literal('literal2')] } },
- {
- default: `[intAttr1] ${sqlOperator} literal1 AND literal2`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: [fn('NOW'), fn('NOW')] },
- };
- testSql(
- { intAttr1: { [operator]: [fn('NOW'), fn('NOW')] } },
- {
- default: `[intAttr1] ${sqlOperator} NOW() AND NOW()`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: [{ [Op.col]: 'col1' }, { [Op.col]: 'col2' }] },
- };
- testSql(
- { intAttr1: { [operator]: [{ [Op.col]: 'col1' }, { [Op.col]: 'col2' }] } },
- {
- default: `[intAttr1] ${sqlOperator} [col1] AND [col2]`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: [cast(col('col'), 'string'), cast(col('col'), 'string')] },
- };
- testSql(
- { intAttr1: { [operator]: [cast(col('col'), 'string'), cast(col('col'), 'string')] } },
- {
- default: `[intAttr1] ${sqlOperator} CAST([col] AS STRING) AND CAST([col] AS STRING)`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.between]: literal('literal1 AND literal2') },
- };
- testSql(
- { intAttr1: { [operator]: literal('literal1 AND literal2') } },
- {
- default: `[intAttr1] ${sqlOperator} literal1 AND literal2`,
- },
- );
- }
- });
- }
- describeBetweenSuite(Op.between, 'BETWEEN');
- describeBetweenSuite(Op.notBetween, 'NOT BETWEEN');
- function describeInSuite(
- operator: typeof Op.in | typeof Op.notIn,
- sqlOperator: string,
- extraTests: () => void,
- ): void {
- // ensure between and notBetween support the same typings, so we only have to test their typings once.
- // unfortunately, at time of writing (TS 4.5.5), TypeScript
- // does not detect an error in `{ [operator]: null }`
- // but it does detect an error in { [Op.gt]: null }`
- expectTypeOf<WhereOperators[typeof Op.between]>().toEqualTypeOf<
- WhereOperators[typeof Op.notBetween]
- >();
- describe(`Op.${operator.description}`, () => {
- {
- const ignoreRight: TestModelWhere = { intAttr1: { [Op.in]: [1, 2, 3] } };
- testSql(
- { intAttr1: { [operator]: [1, 2, 3] } },
- {
- default: `[intAttr1] ${sqlOperator} (1, 2, 3)`,
- },
- );
- }
- if (dialectSupportsArray()) {
- {
- // valid
- const ignore: TestModelWhere = {
- intArrayAttr: {
- [Op.in]: [
- [1, 2],
- [3, 4],
- ],
- },
- };
- testSql(
- {
- intArrayAttr: {
- [operator]: [
- [1, 2],
- [3, 4],
- ],
- },
- },
- {
- default: `[intArrayAttr] ${sqlOperator} (ARRAY[1,2], ARRAY[3,4])`,
- },
- );
- }
- {
- // @ts-expect-error -- intAttr1 is not an array
- const ignore: TestModelWhere = {
- intAttr1: {
- [Op.in]: [
- [1, 2],
- [3, 4],
- ],
- },
- };
- testSql(
- {
- intArrayAttr: {
- [operator]: [
- [1, 2],
- [3, 4],
- ],
- },
- },
- {
- default: `[intArrayAttr] ${sqlOperator} (ARRAY[1,2], ARRAY[3,4])`,
- },
- );
- }
- }
- {
- // @ts-expect-error -- this is invalid because intAttr1 is not an array and cannot be compared to arrays.
- const ignore: TestModelWhere = {
- intAttr1: {
- [Op.in]: [
- [1, 2],
- [3, 4],
- ],
- },
- };
- }
- {
- // @ts-expect-error -- not supported, testing that it throws
- const ignoreWrong: TestModelWhere = { intAttr1: { [Op.in]: 1 } };
- testSql(
- { intAttr1: { [operator]: 1 } },
- {
- default: new Error(
- 'Operators Op.in and Op.notIn must be called with an array of values, or a literal',
- ),
- },
- );
- }
- {
- // @ts-expect-error -- not supported, testing that it throws
- const ignoreWrong: TestModelWhere = { intAttr1: { [Op.in]: col('col2') } };
- testSql(
- { intAttr1: { [operator]: col('col1') } },
- {
- default: new Error(
- 'Operators Op.in and Op.notIn must be called with an array of values, or a literal',
- ),
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = { intAttr1: { [Op.in]: [col('col1'), col('col2')] } };
- testSql(
- { intAttr1: { [operator]: [col('col1'), col('col2')] } },
- {
- default: `[intAttr1] ${sqlOperator} ([col1], [col2])`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.in]: [literal('literal1'), literal('literal2')] },
- };
- testSql(
- { intAttr1: { [operator]: [literal('literal1'), literal('literal2')] } },
- {
- default: `[intAttr1] ${sqlOperator} (literal1, literal2)`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = { intAttr1: { [Op.in]: [fn('NOW'), fn('NOW')] } };
- testSql(
- { intAttr1: { [operator]: [fn('NOW'), fn('NOW')] } },
- {
- default: `[intAttr1] ${sqlOperator} (NOW(), NOW())`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.in]: [{ [Op.col]: 'col1' }, { [Op.col]: 'col2' }] },
- };
- testSql(
- { intAttr1: { [operator]: [{ [Op.col]: 'col1' }, { [Op.col]: 'col2' }] } },
- {
- default: `[intAttr1] ${sqlOperator} ([col1], [col2])`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intAttr1: { [Op.in]: [cast(col('col'), 'string'), cast(col('col'), 'string')] },
- };
- testSql(
- { intAttr1: { [operator]: [cast(col('col'), 'string'), cast(col('col'), 'string')] } },
- {
- default: `[intAttr1] ${sqlOperator} (CAST([col] AS STRING), CAST([col] AS STRING))`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = { intAttr1: { [Op.in]: literal('literal') } };
- testSql(
- { intAttr1: { [operator]: literal('literal') } },
- {
- default: `[intAttr1] ${sqlOperator} literal`,
- },
- );
- }
- {
- // @ts-expect-error -- Op.all is not compatible with Op.in
- const ignoreWrong: TestModelWhere = { intAttr1: { [Op.in]: { [Op.all]: [] } } };
- }
- extraTests();
- });
- }
- describeInSuite(Op.in, 'IN', () => {
- testSql(
- { intAttr1: { [Op.in]: [] } },
- {
- default: '[intAttr1] IN (NULL)',
- },
- );
- });
- describeInSuite(Op.notIn, 'NOT IN', () => {
- testSql(
- { intAttr1: { [Op.notIn]: [] } },
- {
- default: '',
- },
- );
- });
- function describeLikeSuite(
- operator: typeof Op.like | typeof Op.notLike | typeof Op.iLike | typeof Op.notILike,
- sqlOperator: string,
- ) {
- // ensure like ops support the same typings, so we only have to test their typings once.
- // unfortunately, at time of writing (TS 4.5.5), TypeScript
- // does not detect an error in `{ [operator]: null }`
- // but it does detect an error in { [Op.iLike]: null }`
- expectTypeOf<WhereOperators[typeof Op.notLike]>().toEqualTypeOf<
- WhereOperators[typeof Op.like]
- >();
- expectTypeOf<WhereOperators[typeof Op.iLike]>().toEqualTypeOf<
- WhereOperators[typeof Op.like]
- >();
- expectTypeOf<WhereOperators[typeof Op.notILike]>().toEqualTypeOf<
- WhereOperators[typeof Op.like]
- >();
- describe(`Op.${operator.description}`, () => {
- expectTypeOf({ stringAttr: { [Op.like]: '%id' } }).toMatchTypeOf<TestModelWhere>();
- testSql(
- { stringAttr: { [operator]: '%id' } },
- {
- default: `[stringAttr] ${sqlOperator} '%id'`,
- mssql: `[stringAttr] ${sqlOperator} N'%id'`,
- },
- );
- // This test checks that the right data type is used to stringify the right operand
- testSql(
- { 'intAttr1::text': { [operator]: '%id' } },
- {
- default: `CAST([intAttr1] AS TEXT) ${sqlOperator} '%id'`,
- mssql: `CAST([intAttr1] AS TEXT) ${sqlOperator} N'%id'`,
- },
- );
- testSequelizeValueMethods(operator, sqlOperator);
- testSupportsAnyAll(operator, sqlOperator, ['a', 'b', 'c'], 'stringAttr');
- });
- }
- describeLikeSuite(Op.like, 'LIKE');
- describeLikeSuite(Op.notLike, 'NOT LIKE');
- describeLikeSuite(Op.iLike, 'ILIKE');
- describeLikeSuite(Op.notILike, 'NOT ILIKE');
- function describeOverlapSuite(
- operator: typeof Op.overlap | typeof Op.contains | typeof Op.contained,
- sqlOperator: string,
- ) {
- expectTypeOf<WhereOperators[typeof Op.contains]>().toEqualTypeOf<
- WhereOperators[typeof Op.overlap]
- >();
- expectTypeOf<WhereOperators[typeof Op.contained]>().toEqualTypeOf<
- WhereOperators[typeof Op.overlap]
- >();
- if (dialectSupportsArray()) {
- describe(`Op.${operator.description} on ARRAY`, () => {
- {
- const ignoreRight: TestModelWhere = { intArrayAttr: { [Op.overlap]: [1, 2, 3] } };
- testSql(
- { intArrayAttr: { [operator]: [1, 2, 3] } },
- {
- default: `[intArrayAttr] ${sqlOperator} ARRAY[1,2,3]`,
- },
- );
- }
- testSequelizeValueMethods(operator, sqlOperator);
- // ARRAY Overlap ARRAY doesn't support ANY or ALL, except with VALUES
- // testSupportsAnyAll(operator, sqlOperator, [[1, 2], [1, 2]]);
- {
- const ignore: TestModelWhere = {
- // @ts-expect-error -- cannot compare an array with a range!
- intArrayAttr: { [Op.overlap]: [1, { value: 2, inclusive: true }] },
- };
- testSql(
- { intArrayAttr: { [operator]: [1, { value: 2, inclusive: true }] } },
- {
- default: new Error('{ value: 2, inclusive: true } is not a valid integer'),
- },
- );
- }
- {
- // @ts-expect-error -- not supported, testing that it throws
- const ignoreWrong: TestModelWhere = { intArrayAttr: { [Op.overlap]: [col('col')] } };
- testSql(
- { intArrayAttr: { [operator]: [col('col')] } },
- {
- default: new Error(`Col { identifiers: [ 'col' ] } is not a valid integer`),
- },
- );
- }
- {
- const ignoreWrong: TestModelWhere = {
- // @ts-expect-error -- not supported, testing that it throws
- intArrayAttr: { [Op.overlap]: [{ [Op.col]: 'col' }] },
- };
- testSql(
- { intArrayAttr: { [operator]: [{ [Op.col]: 'col' }] } },
- {
- default: new Error(`{ [Symbol(col)]: 'col' } is not a valid integer`),
- },
- );
- }
- {
- // @ts-expect-error -- not supported, testing that it throws
- const ignoreWrong: TestModelWhere = {
- intArrayAttr: { [Op.overlap]: [literal('literal')] },
- };
- testSql(
- { intArrayAttr: { [operator]: [literal('literal')] } },
- {
- default: new Error(`Literal { val: [ 'literal' ] } is not a valid integer`),
- },
- );
- }
- });
- }
- if (dialectSupportsRange()) {
- describe(`Op.${operator.description} on RANGE`, () => {
- {
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.overlap]: [1, 2] } };
- testSql(
- { intRangeAttr: { [operator]: [1, 2] } },
- {
- default: `[intRangeAttr] ${sqlOperator} '[1,2)'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intRangeAttr: { [Op.overlap]: [1, { value: 2, inclusive: true }] },
- };
- testSql(
- { intRangeAttr: { [operator]: [1, { value: 2, inclusive: true }] } },
- {
- // used 'postgres' because otherwise range is transformed to "1,2"
- postgres: `"intRangeAttr" ${sqlOperator} '[1,2]'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intRangeAttr: { [Op.overlap]: [{ value: 1, inclusive: false }, 2] },
- };
- testSql(
- { intRangeAttr: { [operator]: [{ value: 1, inclusive: false }, 2] } },
- {
- default: `[intRangeAttr] ${sqlOperator} '(1,2)'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intRangeAttr: {
- [Op.overlap]: [
- { value: 1, inclusive: false },
- { value: 2, inclusive: false },
- ],
- },
- };
- testSql(
- {
- intRangeAttr: {
- [operator]: [
- { value: 1, inclusive: false },
- { value: 2, inclusive: false },
- ],
- },
- },
- {
- default: `[intRangeAttr] ${sqlOperator} '(1,2)'::int4range`,
- },
- );
- }
- {
- // unbounded range (right)
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.overlap]: [10, null] } };
- testSql(
- {
- intRangeAttr: { [operator]: [10, null] },
- },
- {
- postgres: `"intRangeAttr" ${sqlOperator} '[10,)'::int4range`,
- },
- );
- }
- {
- // unbounded range (left)
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.overlap]: [null, 10] } };
- testSql(
- {
- intRangeAttr: { [operator]: [null, 10] },
- },
- {
- postgres: `"intRangeAttr" ${sqlOperator} '[,10)'::int4range`,
- },
- );
- }
- {
- // unbounded range (left)
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.overlap]: [null, null] } };
- testSql(
- {
- intRangeAttr: { [operator]: [null, null] },
- },
- {
- postgres: `"intRangeAttr" ${sqlOperator} '[,)'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- dateRangeAttr: { [Op.overlap]: [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY] },
- };
- testSql(
- {
- dateRangeAttr: {
- [operator]: [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY],
- },
- },
- {
- postgres: `"dateRangeAttr" ${sqlOperator} '[-infinity,infinity)'::tstzrange`,
- },
- );
- }
- {
- // empty range
- const ignoreRight: TestModelWhere = { dateRangeAttr: { [Op.overlap]: [] } };
- testSql(
- {
- dateRangeAttr: { [operator]: [] },
- },
- {
- postgres: `"dateRangeAttr" ${sqlOperator} 'empty'::tstzrange`,
- },
- );
- }
- {
- // @ts-expect-error -- 'intRangeAttr' is a range, but right-hand side is a regular Array
- const ignore: TestModelWhere = { intRangeAttr: { [Op.overlap]: [1, 2, 3] } };
- testSql(
- { intRangeAttr: { [operator]: [1, 2, 3] } },
- {
- default: new Error(
- 'A range must either be an array with two elements, or an empty array for the empty range. Got [ 1, 2, 3 ].',
- ),
- },
- );
- }
- testSequelizeValueMethods(operator, sqlOperator);
- testSupportsAnyAll(operator, sqlOperator, [1, 2]);
- });
- }
- }
- describeOverlapSuite(Op.overlap, '&&');
- describeOverlapSuite(Op.contains, '@>');
- if (dialectSupportsRange()) {
- describe('RANGE Op.contains ELEMENT', () => {
- testSql(
- {
- intRangeAttr: { [Op.contains]: 1 },
- },
- {
- postgres: `"intRangeAttr" @> 1`,
- },
- );
- testSql(
- // @ts-expect-error -- `ARRAY Op.contains ELEMENT` is not a valid query
- { intArrayAttr: { [Op.contains]: 1 } },
- {
- default: new Error('1 is not a valid array'),
- },
- );
- });
- }
- describeOverlapSuite(Op.contained, '<@');
- describe('ELEMENT Op.contained RANGE', () => {
- if (!dialectSupportsRange()) {
- return;
- }
- testSql(
- {
- intAttr1: { [Op.contained]: [1, 2] },
- },
- {
- postgres: `"intAttr1" <@ '[1,2)'::int4range`,
- },
- );
- testSql(
- {
- bigIntAttr: { [Op.contained]: [1, 2] },
- },
- {
- postgres: `"bigIntAttr" <@ '[1,2)'::int8range`,
- },
- );
- testSql(
- {
- dateAttr: {
- [Op.contained]: [new Date('2020-01-01T00:00:00Z'), new Date('2021-01-01T00:00:00Z')],
- },
- },
- {
- postgres: `"dateAttr" <@ '[2020-01-01 00:00:00.000 +00:00,2021-01-01 00:00:00.000 +00:00)'::tstzrange`,
- },
- );
- /*
- TODO:
- numrange — Range of numeric
- tsrange — Range of timestamp without time zone
- daterange — Range of date
- */
- });
- describe('Op.startsWith', () => {
- // TODO: use implementation not based on "LIKE"
- // mysql, mariadb: locate()
- // postgres:, ^@
- // snowflake, ibmi, db2: position()
- // mssql: CHARINDEX()
- // sqlite3: INSTR()
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: 'swagger',
- },
- },
- {
- default: `[stringAttr] LIKE 'swagger%'`,
- mssql: `[stringAttr] LIKE N'swagger%'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: "sql'injection",
- },
- },
- {
- default: `[stringAttr] LIKE 'sql''injection%'`,
- mysql: `\`stringAttr\` LIKE 'sql\\'injection%'`,
- mariadb: `\`stringAttr\` LIKE 'sql\\'injection%'`,
- mssql: `[stringAttr] LIKE N'sql''injection%'`,
- },
- );
- // startsWith should escape anything that has special meaning in LIKE
- testSql.skip(
- {
- stringAttr: {
- [Op.startsWith]: 'like%injection',
- },
- },
- {
- default: String.raw`[stringAttr] LIKE 'sql\%injection%' ESCAPE '\'`,
- mssql: String.raw`[stringAttr] LIKE N'sql\%injection%' ESCAPE '\'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: literal('$bind'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT($bind, '%')`,
- mssql: `[stringAttr] LIKE CONCAT($bind, N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: col('username'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT([username], '%')`,
- mssql: `[stringAttr] LIKE CONCAT([username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: { [Op.col]: 'username' },
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT([username], '%')`,
- mssql: `[stringAttr] LIKE CONCAT([username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: fn('NOW'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT(NOW(), '%')`,
- mssql: `[stringAttr] LIKE CONCAT(NOW(), N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.startsWith]: cast(fn('NOW'), 'string'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT(CAST(NOW() AS STRING), '%')`,
- mssql: `[stringAttr] LIKE CONCAT(CAST(NOW() AS STRING), N'%')`,
- },
- );
- // these cannot be compatible because it's not possible to provide a ESCAPE clause (although the default ESCAPe is '\')
- testSql(
- // @ts-expect-error -- startsWith is not compatible with Op.any
- { stringAttr: { [Op.startsWith]: { [Op.any]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(any)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- testSql(
- // @ts-expect-error -- startsWith is not compatible with Op.all
- { stringAttr: { [Op.startsWith]: { [Op.all]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(all)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- });
- describe('Op.endsWith', () => {
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: 'swagger',
- },
- },
- {
- default: `[stringAttr] LIKE '%swagger'`,
- mssql: `[stringAttr] LIKE N'%swagger'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: "sql'injection",
- },
- },
- {
- default: `[stringAttr] LIKE '%sql''injection'`,
- mysql: `\`stringAttr\` LIKE '%sql\\'injection'`,
- mariadb: `\`stringAttr\` LIKE '%sql\\'injection'`,
- mssql: `[stringAttr] LIKE N'%sql''injection'`,
- },
- );
- // endsWith should escape anything that has special meaning in LIKE
- testSql.skip(
- {
- stringAttr: {
- [Op.endsWith]: 'like%injection',
- },
- },
- {
- default: String.raw`[stringAttr] LIKE '%sql\%injection' ESCAPE '\'`,
- mssql: String.raw`[stringAttr] LIKE N'%sql\%injection' ESCAPE '\'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: literal('$bind'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', $bind)`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', $bind)`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: col('username'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', [username])`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', [username])`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: { [Op.col]: 'username' },
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', [username])`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', [username])`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: fn('NOW'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', NOW())`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', NOW())`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.endsWith]: cast(fn('NOW'), 'string'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', CAST(NOW() AS STRING))`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', CAST(NOW() AS STRING))`,
- },
- );
- // these cannot be compatible because it's not possible to provide a ESCAPE clause (although the default ESCAPE is '\')
- testSql(
- // @ts-expect-error -- startsWith is not compatible with Op.any
- { stringAttr: { [Op.endsWith]: { [Op.any]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(any)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- testSql(
- // @ts-expect-error -- startsWith is not compatible with Op.all
- { stringAttr: { [Op.endsWith]: { [Op.all]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(all)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- });
- describe('Op.substring', () => {
- // TODO: use implementation not based on "LIKE"
- // mysql, mariadb: locate()
- // postgres:, position()
- // snowflake, ibmi, db2: position()
- // mssql: CHARINDEX()
- // sqlite3: INSTR()
- testSql(
- {
- stringAttr: {
- [Op.substring]: 'swagger',
- },
- },
- {
- default: `[stringAttr] LIKE '%swagger%'`,
- mssql: `[stringAttr] LIKE N'%swagger%'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.substring]: "sql'injection",
- },
- },
- {
- default: `[stringAttr] LIKE '%sql''injection%'`,
- mysql: `\`stringAttr\` LIKE '%sql\\'injection%'`,
- mariadb: `\`stringAttr\` LIKE '%sql\\'injection%'`,
- mssql: `[stringAttr] LIKE N'%sql''injection%'`,
- },
- );
- // substring should escape anything that has special meaning in LIKE
- testSql.skip(
- {
- stringAttr: {
- [Op.substring]: 'like%injection',
- },
- },
- {
- default: String.raw`[stringAttr] LIKE '%sql\%injection%' ESCAPE '\'`,
- mssql: String.raw`[stringAttr] LIKE N'%sql\%injection%' ESCAPE '\'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.substring]: literal('$bind'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', $bind, '%')`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', $bind, N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.substring]: col('username'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', [username], '%')`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', [username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.substring]: { [Op.col]: 'username' },
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', [username], '%')`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', [username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.substring]: fn('NOW'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', NOW(), '%')`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', NOW(), N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.substring]: cast(fn('NOW'), 'string'),
- },
- },
- {
- default: `[stringAttr] LIKE CONCAT('%', CAST(NOW() AS STRING), '%')`,
- mssql: `[stringAttr] LIKE CONCAT(N'%', CAST(NOW() AS STRING), N'%')`,
- },
- );
- // these cannot be compatible because it's not possible to provide a ESCAPE clause (although the default ESCAPE is '\')
- testSql(
- // @ts-expect-error -- startsWith is not compatible with Op.any
- { stringAttr: { [Op.substring]: { [Op.any]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(any)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- testSql(
- // @ts-expect-error -- startsWith is not compatible with Op.all
- { stringAttr: { [Op.substring]: { [Op.all]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(all)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- });
- describe('Op.notStartsWith', () => {
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: 'swagger',
- },
- },
- {
- default: `[stringAttr] NOT LIKE 'swagger%'`,
- mssql: `[stringAttr] NOT LIKE N'swagger%'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: "sql'injection",
- },
- },
- {
- default: `[stringAttr] NOT LIKE 'sql''injection%'`,
- mysql: `\`stringAttr\` NOT LIKE 'sql\\'injection%'`,
- mariadb: `\`stringAttr\` NOT LIKE 'sql\\'injection%'`,
- mssql: `[stringAttr] NOT LIKE N'sql''injection%'`,
- },
- );
- // startsWith should escape anything that has special meaning in LIKE
- testSql.skip(
- {
- stringAttr: {
- [Op.notStartsWith]: 'like%injection',
- },
- },
- {
- default: String.raw`[stringAttr] NOT LIKE 'sql\%injection%' ESCAPE '\'`,
- mssql: String.raw`[stringAttr] NOT LIKE N'sql\%injection%' ESCAPE '\'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: literal('$bind'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT($bind, '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT($bind, N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: col('username'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT([username], '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT([username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: { [Op.col]: 'username' },
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT([username], '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT([username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: fn('NOW'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT(NOW(), '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(NOW(), N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notStartsWith]: cast(fn('NOW'), 'string'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT(CAST(NOW() AS STRING), '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(CAST(NOW() AS STRING), N'%')`,
- },
- );
- // these cannot be compatible because it's not possible to provide a ESCAPE clause (although the default ESCAPe is '\')
- testSql(
- // @ts-expect-error -- notStartsWith is not compatible with Op.any
- { stringAttr: { [Op.notStartsWith]: { [Op.any]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(any)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- testSql(
- // @ts-expect-error -- notStartsWith is not compatible with Op.all
- { stringAttr: { [Op.notStartsWith]: { [Op.all]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(all)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- });
- describe('Op.notEndsWith', () => {
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: 'swagger',
- },
- },
- {
- default: `[stringAttr] NOT LIKE '%swagger'`,
- mssql: `[stringAttr] NOT LIKE N'%swagger'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: "sql'injection",
- },
- },
- {
- default: `[stringAttr] NOT LIKE '%sql''injection'`,
- mysql: `\`stringAttr\` NOT LIKE '%sql\\'injection'`,
- mariadb: `\`stringAttr\` NOT LIKE '%sql\\'injection'`,
- mssql: `[stringAttr] NOT LIKE N'%sql''injection'`,
- },
- );
- // notEndsWith should escape anything that has special meaning in LIKE
- testSql.skip(
- {
- stringAttr: {
- [Op.notEndsWith]: 'like%injection',
- },
- },
- {
- default: String.raw`[stringAttr] NOT LIKE '%sql\%injection' ESCAPE '\'`,
- mssql: String.raw`[stringAttr] NOT LIKE N'%sql\%injection' ESCAPE '\'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: literal('$bind'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', $bind)`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', $bind)`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: col('username'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', [username])`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', [username])`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: { [Op.col]: 'username' },
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', [username])`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', [username])`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: fn('NOW'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', NOW())`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', NOW())`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notEndsWith]: cast(fn('NOW'), 'string'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', CAST(NOW() AS STRING))`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', CAST(NOW() AS STRING))`,
- },
- );
- // these cannot be compatible because it's not possible to provide a ESCAPE clause (although the default ESCAPE is '\')
- testSql(
- // @ts-expect-error -- notEndsWith is not compatible with Op.any
- { stringAttr: { [Op.notEndsWith]: { [Op.any]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(any)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- testSql(
- // @ts-expect-error -- notEndsWith is not compatible with Op.all
- { stringAttr: { [Op.notEndsWith]: { [Op.all]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(all)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- });
- describe('Op.notSubstring', () => {
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: 'swagger',
- },
- },
- {
- default: `[stringAttr] NOT LIKE '%swagger%'`,
- mssql: `[stringAttr] NOT LIKE N'%swagger%'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: "sql'injection",
- },
- },
- {
- default: `[stringAttr] NOT LIKE '%sql''injection%'`,
- mysql: `\`stringAttr\` NOT LIKE '%sql\\'injection%'`,
- mariadb: `\`stringAttr\` NOT LIKE '%sql\\'injection%'`,
- mssql: `[stringAttr] NOT LIKE N'%sql''injection%'`,
- },
- );
- // notSubstring should escape anything that has special meaning in LIKE
- testSql.skip(
- {
- stringAttr: {
- [Op.notSubstring]: 'like%injection',
- },
- },
- {
- default: String.raw`[stringAttr] NOT LIKE '%sql\%injection%' ESCAPE '\'`,
- mssql: String.raw`[stringAttr] NOT LIKE N'%sql\%injection%' ESCAPE '\'`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: literal('$bind'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', $bind, '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', $bind, N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: col('username'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', [username], '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', [username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: { [Op.col]: 'username' },
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', [username], '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', [username], N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: fn('NOW'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', NOW(), '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', NOW(), N'%')`,
- },
- );
- testSql(
- {
- stringAttr: {
- [Op.notSubstring]: cast(fn('NOW'), 'string'),
- },
- },
- {
- default: `[stringAttr] NOT LIKE CONCAT('%', CAST(NOW() AS STRING), '%')`,
- mssql: `[stringAttr] NOT LIKE CONCAT(N'%', CAST(NOW() AS STRING), N'%')`,
- },
- );
- // these cannot be compatible because it's not possible to provide a ESCAPE clause (although the default ESCAPE is '\')
- testSql(
- // @ts-expect-error -- notSubstring is not compatible with Op.any
- { stringAttr: { [Op.notSubstring]: { [Op.any]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(any)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- testSql(
- // @ts-expect-error -- notSubstring is not compatible with Op.all
- { stringAttr: { [Op.notSubstring]: { [Op.all]: ['test'] } } },
- {
- default: new Error(
- `{ [Symbol(all)]: [ 'test' ] } is not a valid string. Only the string type is accepted for non-binary strings.`,
- ),
- },
- );
- });
- function describeRegexpSuite(
- operator: typeof Op.regexp | typeof Op.iRegexp | typeof Op.notRegexp | typeof Op.notIRegexp,
- sqlOperator: string,
- ) {
- expectTypeOf<WhereOperators[typeof Op.iRegexp]>().toEqualTypeOf<
- WhereOperators[typeof Op.regexp]
- >();
- expectTypeOf<WhereOperators[typeof Op.notRegexp]>().toEqualTypeOf<
- WhereOperators[typeof Op.regexp]
- >();
- expectTypeOf<WhereOperators[typeof Op.notIRegexp]>().toEqualTypeOf<
- WhereOperators[typeof Op.regexp]
- >();
- describe(`Op.${operator.description}`, () => {
- {
- const ignore: TestModelWhere = { stringAttr: { [Op.regexp]: '^sw.*r$' } };
- }
- testSql(
- { stringAttr: { [operator]: '^sw.*r$' } },
- {
- default: `[stringAttr] ${sqlOperator} '^sw.*r$'`,
- },
- );
- testSql(
- { stringAttr: { [operator]: '^new\nline$' } },
- {
- default: `[stringAttr] ${sqlOperator} '^new\nline$'`,
- mariadb: `\`stringAttr\` ${sqlOperator} '^new\\nline$'`,
- mysql: `\`stringAttr\` ${sqlOperator} '^new\\nline$'`,
- },
- );
- testSequelizeValueMethods(operator, sqlOperator);
- testSupportsAnyAll(operator, sqlOperator, ['^a$', '^b$'], 'stringAttr');
- });
- }
- if (sequelize.dialect.supports.REGEXP) {
- describeRegexpSuite(Op.regexp, sequelize.dialect.name === 'postgres' ? '~' : 'REGEXP');
- describeRegexpSuite(
- Op.notRegexp,
- sequelize.dialect.name === 'postgres' ? '!~' : 'NOT REGEXP',
- );
- }
- if (sequelize.dialect.supports.IREGEXP) {
- describeRegexpSuite(Op.iRegexp, '~*');
- describeRegexpSuite(Op.notIRegexp, '!~*');
- }
- if (sequelize.dialect.supports.dataTypes.TSVECTOR) {
- describe('Op.match', () => {
- testSql(
- { stringAttr: { [Op.match]: fn('to_tsvector', 'swagger') } },
- {
- default: `[stringAttr] @@ to_tsvector('swagger')`,
- },
- );
- testSequelizeValueMethods(Op.match, '@@');
- // TODO
- // testSupportsAnyAll(Op.match, '@@', [fn('to_tsvector', 'a'), fn('to_tsvector', 'b')]);
- });
- }
- function describeAdjacentRangeSuite(
- operator:
- | typeof Op.adjacent
- | typeof Op.strictLeft
- | typeof Op.strictRight
- | typeof Op.noExtendLeft
- | typeof Op.noExtendRight,
- sqlOperator: string,
- ) {
- if (!dialectSupportsRange()) {
- return;
- }
- expectTypeOf<WhereOperators[typeof Op.strictLeft]>().toEqualTypeOf<
- WhereOperators[typeof Op.adjacent]
- >();
- expectTypeOf<WhereOperators[typeof Op.strictRight]>().toEqualTypeOf<
- WhereOperators[typeof Op.adjacent]
- >();
- expectTypeOf<WhereOperators[typeof Op.noExtendLeft]>().toEqualTypeOf<
- WhereOperators[typeof Op.adjacent]
- >();
- expectTypeOf<WhereOperators[typeof Op.noExtendRight]>().toEqualTypeOf<
- WhereOperators[typeof Op.adjacent]
- >();
- describe(`RANGE Op.${operator.description} RANGE`, () => {
- {
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.adjacent]: [1, 2] } };
- testSql(
- { intRangeAttr: { [operator]: [1, 2] } },
- {
- default: `[intRangeAttr] ${sqlOperator} '[1,2)'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intRangeAttr: { [Op.adjacent]: [1, { value: 2, inclusive: true }] },
- };
- testSql(
- { intRangeAttr: { [operator]: [1, { value: 2, inclusive: true }] } },
- {
- // used 'postgres' because otherwise range is transformed to "1,2"
- postgres: `"intRangeAttr" ${sqlOperator} '[1,2]'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intRangeAttr: { [Op.adjacent]: [{ value: 1, inclusive: false }, 2] },
- };
- testSql(
- { intRangeAttr: { [operator]: [{ value: 1, inclusive: false }, 2] } },
- {
- default: `[intRangeAttr] ${sqlOperator} '(1,2)'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- intRangeAttr: {
- [Op.adjacent]: [
- { value: 1, inclusive: false },
- { value: 2, inclusive: false },
- ],
- },
- };
- testSql(
- {
- intRangeAttr: {
- [operator]: [
- { value: 1, inclusive: false },
- { value: 2, inclusive: false },
- ],
- },
- },
- {
- default: `[intRangeAttr] ${sqlOperator} '(1,2)'::int4range`,
- },
- );
- }
- {
- // unbounded range (right)
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.adjacent]: [10, null] } };
- testSql(
- {
- intRangeAttr: { [operator]: [10, null] },
- },
- {
- postgres: `"intRangeAttr" ${sqlOperator} '[10,)'::int4range`,
- },
- );
- }
- {
- // unbounded range (left)
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.adjacent]: [null, 10] } };
- testSql(
- {
- intRangeAttr: { [operator]: [null, 10] },
- },
- {
- postgres: `"intRangeAttr" ${sqlOperator} '[,10)'::int4range`,
- },
- );
- }
- {
- // unbounded range (left)
- const ignoreRight: TestModelWhere = { intRangeAttr: { [Op.adjacent]: [null, null] } };
- testSql(
- {
- intRangeAttr: { [operator]: [null, null] },
- },
- {
- postgres: `"intRangeAttr" ${sqlOperator} '[,)'::int4range`,
- },
- );
- }
- {
- const ignoreRight: TestModelWhere = {
- dateRangeAttr: { [Op.adjacent]: [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY] },
- };
- testSql(
- {
- dateRangeAttr: {
- [operator]: [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY],
- },
- },
- {
- postgres: `"dateRangeAttr" ${sqlOperator} '[-infinity,infinity)'::tstzrange`,
- },
- );
- }
- {
- // empty range
- const ignoreRight: TestModelWhere = { dateRangeAttr: { [Op.adjacent]: [] } };
- testSql(
- {
- dateRangeAttr: { [operator]: [] },
- },
- {
- postgres: `"dateRangeAttr" ${sqlOperator} 'empty'::tstzrange`,
- },
- );
- }
- {
- // @ts-expect-error -- 'intRangeAttr' is a range, but right-hand side is a regular Array
- const ignore: TestModelWhere = { intRangeAttr: { [Op.overlap]: [1, 2, 3] } };
- testSql(
- { intRangeAttr: { [operator]: [1, 2, 3] } },
- {
- default: new Error(
- 'A range must either be an array with two elements, or an empty array for the empty range. Got [ 1, 2, 3 ].',
- ),
- },
- );
- }
- });
- }
- describeAdjacentRangeSuite(Op.adjacent, '-|-');
- describeAdjacentRangeSuite(Op.strictLeft, '<<');
- describeAdjacentRangeSuite(Op.strictRight, '>>');
- describeAdjacentRangeSuite(Op.noExtendLeft, '&>');
- describeAdjacentRangeSuite(Op.noExtendRight, '&<');
- if (sequelize.dialect.supports.jsonOperations) {
- describe('JSON Operations', () => {
- {
- // @ts-expect-error -- attribute 'doesNotExist' does not exist.
- const ignore: TestModelWhere = { 'doesNotExist.nested': 'value' };
- }
- {
- // @ts-expect-error -- attribute 'doesNotExist' does not exist.
- const ignore: TestModelWhere = { '$doesNotExist$.nested': 'value' };
- }
- testSql(
- { jsonAttr: 'value' },
- {
- default: `[jsonAttr] = '"value"'`,
- mysql: `\`jsonAttr\` = CAST('"value"' AS JSON)`,
- mssql: `[jsonAttr] = N'"value"'`,
- },
- );
- testSql(
- { jsonAttr: null },
- {
- default: new Error('You must be explicit'),
- },
- );
- testSql(
- { jsonAttr: { [Op.eq]: null } },
- {
- default: `[jsonAttr] = 'null'`,
- mysql: `\`jsonAttr\` = CAST('null' AS JSON)`,
- mssql: `[jsonAttr] = N'null'`,
- },
- );
- testSql(
- { jsonAttr: { [Op.is]: null } },
- {
- default: `[jsonAttr] IS NULL`,
- },
- );
- testSql(
- { jsonAttr: JSON_NULL },
- {
- default: `[jsonAttr] = 'null'`,
- mysql: `\`jsonAttr\` = CAST('null' AS JSON)`,
- mssql: `[jsonAttr] = N'null'`,
- },
- );
- testSql(
- { jsonAttr: SQL_NULL },
- {
- default: `[jsonAttr] IS NULL`,
- },
- );
- if (dialectSupportsJsonQuotedExtraction()) {
- testSql(
- { 'jsonAttr.nested': 'value' },
- {
- postgres: `"jsonAttr"->'nested' = '"value"'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = '"value"'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = '"value"'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- { 'jsonAttr.nested': null },
- {
- default: new Error('You must be explicit'),
- },
- );
- testSql(
- { 'jsonAttr.nested': JSON_NULL },
- {
- postgres: `"jsonAttr"->'nested' = 'null'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = 'null'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = 'null'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('null' AS JSON)`,
- },
- );
- testSql(
- { 'jsonAttr.nested': SQL_NULL },
- {
- postgres: `"jsonAttr"->'nested' IS NULL`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) IS NULL`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`,
- },
- );
- testSql(
- { 'jsonAttr.nested': { [Op.eq]: null } },
- {
- postgres: `"jsonAttr"->'nested' = 'null'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = 'null'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = 'null'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('null' AS JSON)`,
- },
- );
- testSql(
- { 'jsonAttr.nested': { [Op.is]: null } },
- {
- postgres: `"jsonAttr"->'nested' IS NULL`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) IS NULL`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`,
- },
- );
- testSql(where('value', Op.eq, attribute('jsonAttr.nested')), {
- postgres: `'"value"' = "jsonAttr"->'nested'`,
- sqlite3: `'"value"' = json_extract(\`jsonAttr\`,'$.nested')`,
- mariadb: `'"value"' = json_compact(json_extract(\`jsonAttr\`,'$.nested'))`,
- mysql: `CAST('"value"' AS JSON) = json_extract(\`jsonAttr\`,'$.nested')`,
- });
- testSql(
- { 'jsonAttr.nested.twice': 'value' },
- {
- postgres: `"jsonAttr"#>ARRAY['nested','twice']::VARCHAR(255)[] = '"value"'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested.twice') = '"value"'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested.twice')) = '"value"'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested.twice') = CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- {
- jsonAttr: { nested: 'value' },
- },
- {
- postgres: `"jsonAttr"->'nested' = '"value"'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = '"value"'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = '"value"'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- {
- 'jsonAttr.nested': { twice: 'value' },
- },
- {
- postgres: `"jsonAttr"#>ARRAY['nested','twice']::VARCHAR(255)[] = '"value"'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested.twice') = '"value"'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested.twice')) = '"value"'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested.twice') = CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- {
- jsonAttr: { [Op.eq]: { key: 'value' } },
- },
- {
- default: `[jsonAttr] = '{"key":"value"}'`,
- mysql: `\`jsonAttr\` = CAST('{"key":"value"}' AS JSON)`,
- },
- );
- testSql(
- {
- 'jsonAttr.nested': { [Op.ne]: 'value' },
- },
- {
- postgres: `"jsonAttr"->'nested' != '"value"'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') != '"value"'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) != '"value"'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') != CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- {
- '$jsonAttr$.nested': 'value',
- },
- {
- postgres: `"jsonAttr"->'nested' = '"value"'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = '"value"'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = '"value"'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- {
- '$association.jsonAttr$.nested': 'value',
- },
- {
- postgres: `"association"."jsonAttr"->'nested' = '"value"'`,
- sqlite3: `json_extract(\`association\`.\`jsonAttr\`,'$.nested') = '"value"'`,
- mariadb: `json_compact(json_extract(\`association\`.\`jsonAttr\`,'$.nested')) = '"value"'`,
- mysql: `json_extract(\`association\`.\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`,
- },
- );
- testSql(
- {
- 'jsonAttr.nested::STRING': 'value',
- },
- {
- // with the left value cast to a string, we serialize the right value as a string, not as a JSON value
- postgres: `CAST("jsonAttr"->'nested' AS STRING) = 'value'`,
- mariadb: `CAST(json_compact(json_extract(\`jsonAttr\`,'$.nested')) AS STRING) = 'value'`,
- 'sqlite3 mysql': `CAST(json_extract(\`jsonAttr\`,'$.nested') AS STRING) = 'value'`,
- },
- );
- testSql(
- {
- '$association.jsonAttr$.nested::STRING': {
- attribute: 'value',
- },
- },
- {
- default: new Error(`Could not guess type of value { attribute: 'value' }`),
- },
- );
- testSql(
- {
- '$association.jsonAttr$.nested.deep::STRING': 'value',
- },
- {
- postgres: `CAST("association"."jsonAttr"#>ARRAY['nested','deep']::VARCHAR(255)[] AS STRING) = 'value'`,
- mariadb: `CAST(json_compact(json_extract(\`association\`.\`jsonAttr\`,'$.nested.deep')) AS STRING) = 'value'`,
- 'sqlite3 mysql': `CAST(json_extract(\`association\`.\`jsonAttr\`,'$.nested.deep') AS STRING) = 'value'`,
- },
- );
- testSql(
- {
- $jsonAttr$: { 'nested::string': 'value' },
- },
- {
- postgres: `CAST("jsonAttr"->'nested' AS STRING) = 'value'`,
- mariadb: `CAST(json_compact(json_extract(\`jsonAttr\`,'$.nested')) AS STRING) = 'value'`,
- 'sqlite3 mysql': `CAST(json_extract(\`jsonAttr\`,'$.nested') AS STRING) = 'value'`,
- },
- );
- testSql(
- { 'jsonAttr.nested.attribute': 4 },
- {
- postgres: `"jsonAttr"#>ARRAY['nested','attribute']::VARCHAR(255)[] = '4'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$.nested.attribute') = '4'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested.attribute')) = '4'`,
- mysql: `json_extract(\`jsonAttr\`,'$.nested.attribute') = CAST('4' AS JSON)`,
- },
- );
- // 0 is treated as a string key here, not an array index
- testSql(
- { 'jsonAttr.0': 4 },
- {
- postgres: `"jsonAttr"->'0' = '4'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$."0"') = '4'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."0"')) = '4'`,
- mysql: `json_extract(\`jsonAttr\`,'$."0"') = CAST('4' AS JSON)`,
- },
- );
- // 0 is treated as an index here, not a string key
- testSql(
- { 'jsonAttr[0]': 4 },
- {
- postgres: `"jsonAttr"->0 = '4'`,
- // these tests cannot be deduplicated because [0] will be replaced by `0` by expectsql
- sqlite3: `json_extract(\`jsonAttr\`,'$[0]') = '4'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$[0]')) = '4'`,
- mysql: `json_extract(\`jsonAttr\`,'$[0]') = CAST('4' AS JSON)`,
- },
- );
- testSql(
- { 'jsonAttr.0.attribute': 4 },
- {
- postgres: `"jsonAttr"#>ARRAY['0','attribute']::VARCHAR(255)[] = '4'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$."0".attribute') = '4'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."0".attribute')) = '4'`,
- mysql: `json_extract(\`jsonAttr\`,'$."0".attribute') = CAST('4' AS JSON)`,
- },
- );
- // Regression test: https://github.com/sequelize/sequelize/issues/8718
- testSql(
- { jsonAttr: { 'hyphenated-key': 4 } },
- {
- postgres: `"jsonAttr"->'hyphenated-key' = '4'`,
- sqlite3: `json_extract(\`jsonAttr\`,'$."hyphenated-key"') = '4'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."hyphenated-key"')) = '4'`,
- mysql: `json_extract(\`jsonAttr\`,'$."hyphenated-key"') = CAST('4' AS JSON)`,
- },
- );
- // SQL injection test
- testSql(
- { jsonAttr: { '"a\')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "': 1 } },
- {
- postgres: `"jsonAttr"->'a'')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- ' = '1'`,
- mysql: `json_extract(\`jsonAttr\`,'$."a\\')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "') = CAST('1' AS JSON)`,
- sqlite3: `json_extract(\`jsonAttr\`,'$."a'')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "') = '1'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."a\\')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "')) = '1'`,
- },
- );
- testSql(
- { 'jsonAttr[0].nested.attribute': 4 },
- {
- postgres: `"jsonAttr"#>ARRAY['0','nested','attribute']::VARCHAR(255)[] = '4'`,
- // these tests cannot be deduplicated because [0] will be replaced by `0` by expectsql
- sqlite3: `json_extract(\`jsonAttr\`,'$[0].nested.attribute') = '4'`,
- mariadb: `json_compact(json_extract(\`jsonAttr\`,'$[0].nested.attribute')) = '4'`,
- mysql: `json_extract(\`jsonAttr\`,'$[0].nested.attribute') = CAST('4' AS JSON)`,
- },
- );
- // aliases attribute -> column correctly
- testSql(
- { 'aliasedJsonAttr.nested.attribute': 4 },
- {
- postgres: `"aliased_json"#>ARRAY['nested','attribute']::VARCHAR(255)[] = '4'`,
- sqlite3: `json_extract(\`aliased_json\`,'$.nested.attribute') = '4'`,
- mariadb: `json_compact(json_extract(\`aliased_json\`,'$.nested.attribute')) = '4'`,
- mysql: `json_extract(\`aliased_json\`,'$.nested.attribute') = CAST('4' AS JSON)`,
- },
- );
- }
- if (dialectSupportsJsonUnquotedExtraction()) {
- testSql(
- { 'jsonAttr:unquote': 0 },
- {
- postgres: `"jsonAttr"#>>ARRAY[]::TEXT[] = 0`,
- mssql: `JSON_VALUE([jsonAttr]) = 0`,
- 'sqlite3 mysql mariadb': `json_unquote([jsonAttr]) = 0`,
- },
- );
- testSql(
- { 'jsonAttr.key:unquote': 0 },
- {
- postgres: `"jsonAttr"->>'key' = 0`,
- mssql: `JSON_VALUE([jsonAttr], N'$.key') = 0`,
- 'sqlite3 mysql mariadb': `json_unquote(json_extract([jsonAttr],'$.key')) = 0`,
- },
- );
- testSql(
- { 'jsonAttr.nested.key:unquote': 0 },
- {
- postgres: `"jsonAttr"#>>ARRAY['nested','key']::VARCHAR(255)[] = 0`,
- mssql: `JSON_VALUE([jsonAttr], N'$.nested.key') = 0`,
- 'sqlite3 mysql mariadb': `json_unquote(json_extract([jsonAttr],'$.nested.key')) = 0`,
- },
- );
- testSql(
- { 'jsonAttr[0]:unquote': 0 },
- {
- postgres: `"jsonAttr"->>0 = 0`,
- // must be separate because [0] will be replaced by `0` by expectsql
- sqlite3: `json_unquote(json_extract(\`jsonAttr\`,'$[0]')) = 0`,
- mysql: `json_unquote(json_extract(\`jsonAttr\`,'$[0]')) = 0`,
- mariadb: `json_unquote(json_extract(\`jsonAttr\`,'$[0]')) = 0`,
- mssql: `JSON_VALUE([jsonAttr], N'$[0]') = 0`,
- },
- );
- }
- });
- }
- if (dialectSupportsJsonB()) {
- describe('JSONB', () => {
- testSql(
- {
- jsonbAttr: {
- [Op.anyKeyExists]: ['a', 'b'],
- },
- },
- {
- default: `[jsonbAttr] ?| ARRAY['a','b']`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.allKeysExist]: ['a', 'b'],
- },
- },
- {
- default: `[jsonbAttr] ?& ARRAY['a','b']`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.anyKeyExists]: literal(
- `ARRAY(SELECT jsonb_array_elements_text('ARRAY["a","b"]'))`,
- ),
- },
- },
- {
- default: `[jsonbAttr] ?| ARRAY(SELECT jsonb_array_elements_text('ARRAY["a","b"]'))`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.allKeysExist]: literal(
- `ARRAY(SELECT jsonb_array_elements_text('ARRAY["a","b"]'))`,
- ),
- },
- },
- {
- default: `[jsonbAttr] ?& ARRAY(SELECT jsonb_array_elements_text('ARRAY["a","b"]'))`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.anyKeyExists]: col('label'),
- },
- },
- {
- default: `[jsonbAttr] ?| "label"`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.allKeysExist]: col('labels'),
- },
- },
- {
- default: `[jsonbAttr] ?& "labels"`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.anyKeyExists]: cast(col('labels'), 'STRING[]'),
- },
- },
- {
- default: `[jsonbAttr] ?| CAST("labels" AS STRING[])`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.allKeysExist]: cast(col('labels'), 'STRING[]'),
- },
- },
- {
- default: `[jsonbAttr] ?& CAST("labels" AS STRING[])`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.anyKeyExists]: [],
- },
- },
- {
- default: `[jsonbAttr] ?| ARRAY[]::TEXT[]`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.allKeysExist]: [],
- },
- },
- {
- default: `[jsonbAttr] ?& ARRAY[]::TEXT[]`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.anyKeyExists]: fn('get_label'),
- },
- },
- {
- default: `[jsonbAttr] ?| get_label()`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.allKeysExist]: fn('get_labels'),
- },
- },
- {
- default: `[jsonbAttr] ?& get_labels()`,
- },
- );
- testSql(
- // @ts-expect-error -- typings for `json` are broken, but `json()` is deprecated
- { id: { [Op.eq]: json('profile.id') } },
- {
- default: `"id" = "profile"->'id'`,
- },
- );
- testSql(
- // @ts-expect-error -- typings for `json` are broken, but `json()` is deprecated
- json('profile.id', cast('12346-78912', 'text')),
- {
- postgres: `"User"."profile"->'id' = CAST('12346-78912' AS TEXT)`,
- },
- {
- mainAlias: 'User',
- },
- );
- testSql(
- json({ profile: { id: '12346-78912', name: 'test' } }),
- {
- postgres: `"User"."profile"->'id' = '"12346-78912"' AND "User"."profile"->'name' = '"test"'`,
- },
- {
- mainAlias: 'User',
- },
- );
- testSql(
- {
- jsonbAttr: {
- nested: {
- attribute: 'value',
- },
- },
- },
- {
- postgres: `"User"."jsonbAttr"#>ARRAY['nested','attribute']::VARCHAR(255)[] = '"value"'`,
- },
- {
- mainAlias: 'User',
- },
- );
- testSql(
- {
- jsonbAttr: {
- nested: {
- [Op.in]: [1, 2],
- },
- },
- },
- {
- postgres: `"jsonbAttr"->'nested' IN ('1', '2')`,
- },
- );
- testSql(
- {
- 'jsonbAttr.nested.attribute': {
- [Op.in]: [3, 7],
- },
- },
- {
- postgres: `"jsonbAttr"#>ARRAY['nested','attribute']::VARCHAR(255)[] IN ('3', '7')`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- nested: {
- [Op.between]: [1, 2],
- },
- },
- },
- {
- postgres: `"jsonbAttr"->'nested' BETWEEN '1' AND '2'`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- price: 5,
- name: 'Product',
- },
- },
- {
- postgres: `"jsonbAttr"->'price' = '5' AND "jsonbAttr"->'name' = '"Product"'`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- name: {
- last: 'Simpson',
- },
- employment: {
- [Op.ne]: 'None',
- },
- },
- },
- {
- postgres: `"User"."jsonbAttr"#>ARRAY['name','last']::VARCHAR(255)[] = '"Simpson"' AND "User"."jsonbAttr"->'employment' != '"None"'`,
- },
- {
- mainAlias: 'User',
- },
- );
- const dt = new Date();
- const jsonDt = JSON.stringify(dt);
- testSql(
- {
- jsonbAttr: {
- nested: {
- attribute: {
- [Op.gt]: dt,
- },
- },
- },
- },
- {
- postgres: `"jsonbAttr"#>ARRAY['nested','attribute']::VARCHAR(255)[] > ${queryGen.escape(jsonDt)}`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- nested: {
- attribute: true,
- },
- },
- },
- {
- postgres: `"jsonbAttr"#>ARRAY['nested','attribute']::VARCHAR(255)[] = 'true'`,
- },
- );
- testSql(
- {
- jsonbAttr: {
- [Op.contains]: { company: 'Magnafone' },
- },
- },
- {
- default: `[jsonbAttr] @> '{"company":"Magnafone"}'`,
- },
- );
- testSql(
- {
- jsonbTypeLiteralAttr: { [Op.contains]: { foo: 'bar' } },
- },
- {
- postgres: '"jsonbTypeLiteralAttr" @> \'{"foo":"bar"}\'',
- },
- );
- testSql(
- {
- // @ts-expect-error -- key `bad` isn't known
- jsonbTypeLiteralAttr: { [Op.contains]: { bad: 'bad' } },
- },
- {
- postgres: '"jsonbTypeLiteralAttr" @> \'{"bad":"bad"}\'',
- },
- );
- testSql(
- {
- jsonbInterfaceAttr: { [Op.contains]: { foo: 'bar' } },
- },
- {
- postgres: '"jsonbInterfaceAttr" @> \'{"foo":"bar"}\'',
- },
- );
- testSql(
- {
- // @ts-expect-error -- key `bad` isn't known
- jsonbInterfaceAttr: { [Op.contains]: { bad: 'bad' } },
- },
- {
- postgres: '"jsonbInterfaceAttr" @> \'{"bad":"bad"}\'',
- },
- );
- // aliases correctly
- testSql(
- { aliasedJsonbAttr: { key: 'value' } },
- {
- postgres: `"aliased_jsonb"->'key' = '"value"'`,
- },
- );
- });
- }
- testSql(
- {
- stringAttr: 'a project',
- [Op.or]: [{ intAttr1: [1, 2, 3] }, { intAttr1: { [Op.gt]: 10 } }],
- },
- {
- default: "([intAttr1] IN (1, 2, 3) OR [intAttr1] > 10) AND [stringAttr] = 'a project'",
- mssql: "([intAttr1] IN (1, 2, 3) OR [intAttr1] > 10) AND [stringAttr] = N'a project'",
- },
- );
- describe('Op.and', () => {
- it('and() is the same as Op.and', () => {
- expect(util.inspect(and('a', 'b'))).to.deep.equal(util.inspect({ [Op.and]: ['a', 'b'] }));
- });
- testSql(and([]), {
- default: '',
- });
- testSql(and({}), {
- default: '',
- });
- // by default: it already is Op.and
- testSql(
- { intAttr1: 1, intAttr2: 2 },
- {
- default: `[intAttr1] = 1 AND [intAttr2] = 2`,
- },
- );
- // top-level array is Op.and
- testSql([{ intAttr1: 1 }, { intAttr1: 2 }], {
- default: `[intAttr1] = 1 AND [intAttr1] = 2`,
- });
- // $intAttr1$ doesn't override intAttr1
- testSql(
- { intAttr1: 1, $intAttr1$: 2 },
- {
- default: `[intAttr1] = 1 AND [intAttr1] = 2`,
- },
- );
- // can pass a simple object
- testSql(
- { [Op.and]: { intAttr1: 1, intAttr2: 2 } },
- {
- default: `[intAttr1] = 1 AND [intAttr2] = 2`,
- },
- );
- // can pass an array
- testSql(
- { [Op.and]: [{ intAttr1: 1, intAttr2: 2 }, { stringAttr: '' }] },
- {
- default: `([intAttr1] = 1 AND [intAttr2] = 2) AND [stringAttr] = ''`,
- mssql: `([intAttr1] = 1 AND [intAttr2] = 2) AND [stringAttr] = N''`,
- },
- );
- // can be used on attribute
- testSql(
- { intAttr1: { [Op.and]: [1, { [Op.gt]: 1 }] } },
- {
- default: `[intAttr1] = 1 AND [intAttr1] > 1`,
- },
- );
- testSql(
- // @ts-expect-error -- cannot be used after operator
- { intAttr1: { [Op.gt]: { [Op.and]: [1, 2] } } },
- {
- default: new Error(`{ [Symbol(and)]: [ 1, 2 ] } is not a valid integer`),
- },
- );
- });
- describe('Op.or', () => {
- it('or() is the same as Op.or', () => {
- expect(util.inspect(or('a', 'b'))).to.deep.equal(util.inspect({ [Op.or]: ['a', 'b'] }));
- });
- testSql(or([]), {
- default: '',
- });
- testSql(or({}), {
- default: '',
- });
- // can pass a simple object
- testSql(
- { [Op.or]: { intAttr1: 1, intAttr2: 2 } },
- {
- default: `[intAttr1] = 1 OR [intAttr2] = 2`,
- },
- );
- // can pass an array
- testSql(
- { [Op.or]: [{ intAttr1: 1, intAttr2: 2 }, { stringAttr: '' }] },
- {
- default: `([intAttr1] = 1 AND [intAttr2] = 2) OR [stringAttr] = ''`,
- mssql: `([intAttr1] = 1 AND [intAttr2] = 2) OR [stringAttr] = N''`,
- },
- );
- // can be used on attribute
- testSql(
- { intAttr1: { [Op.or]: [1, { [Op.gt]: 1 }] } },
- {
- default: `[intAttr1] = 1 OR [intAttr1] > 1`,
- },
- );
- testSql(
- // @ts-expect-error -- cannot be used after operator
- { intAttr1: { [Op.gt]: { [Op.or]: [1, 2] } } },
- {
- default: new Error(`{ [Symbol(or)]: [ 1, 2 ] } is not a valid integer`),
- },
- );
- testSql(
- {
- [Op.or]: {
- intAttr1: [1, 3],
- intAttr2: {
- [Op.in]: [2, 4],
- },
- },
- },
- {
- default: '[intAttr1] IN (1, 3) OR [intAttr2] IN (2, 4)',
- },
- );
- });
- describe('Op.{and,or,not} combinations', () => {
- // both can be used in the same object
- testSql(
- {
- [Op.and]: { intAttr1: 1, intAttr2: 2 },
- [Op.or]: { intAttr1: 1, intAttr2: 2 },
- },
- {
- default: `([intAttr1] = 1 AND [intAttr2] = 2) AND ([intAttr1] = 1 OR [intAttr2] = 2)`,
- },
- );
- // Op.or only applies to its direct Array, the nested array is still Op.and
- testSql(
- {
- [Op.or]: [[{ intAttr1: 1 }, { intAttr1: 2 }], { intAttr1: 3 }],
- },
- {
- default: '([intAttr1] = 1 AND [intAttr1] = 2) OR [intAttr1] = 3',
- },
- );
- // can be nested *after* attribute
- testSql(
- {
- intAttr1: {
- [Op.and]: [1, 2, { [Op.or]: [3, 4] }, { [Op.not]: 5 }, [6, 7]],
- },
- },
- {
- default:
- '[intAttr1] = 1 AND [intAttr1] = 2 AND ([intAttr1] = 3 OR [intAttr1] = 4) AND NOT ([intAttr1] = 5) AND [intAttr1] IN (6, 7)',
- },
- );
- // can be nested
- testSql(
- {
- [Op.not]: {
- [Op.and]: {
- [Op.or]: {
- [Op.and]: {
- intAttr1: 1,
- intAttr2: 2,
- },
- },
- },
- },
- },
- {
- default: 'NOT ([intAttr1] = 1 AND [intAttr2] = 2)',
- },
- );
- testSql(
- {
- [Op.not]: {
- [Op.or]: {
- [Op.and]: {
- intAttr1: 1,
- intAttr2: 2,
- },
- [Op.or]: {
- intAttr1: 1,
- intAttr2: 2,
- },
- },
- },
- },
- {
- default:
- 'NOT (([intAttr1] = 1 AND [intAttr2] = 2) OR ([intAttr1] = 1 OR [intAttr2] = 2))',
- },
- );
- // Op.not, Op.and, Op.or can reside on the same object as attributes
- testSql(
- {
- intAttr1: 1,
- [Op.not]: {
- intAttr1: { [Op.eq]: 2 },
- [Op.and]: {
- intAttr1: 3,
- [Op.or]: {
- intAttr1: 4,
- [Op.and]: {
- intAttr1: 5,
- intAttr2: 6,
- },
- },
- },
- },
- },
- {
- default:
- '(NOT (((([intAttr1] = 5 AND [intAttr2] = 6) OR [intAttr1] = 4) AND [intAttr1] = 3) AND [intAttr1] = 2)) AND [intAttr1] = 1',
- },
- );
- });
- describe('where()', () => {
- {
- // @ts-expect-error -- 'intAttr1' is not a boolean and cannot be compared to the output of 'where'
- const ignore: TestModelWhere = { intAttr1: where(fn('lower', col('name')), null) };
- }
- testSql(
- { booleanAttr: where(fn('lower', col('name')), null) },
- {
- default: `[booleanAttr] = (lower([name]) IS NULL)`,
- },
- );
- testSql(
- { booleanAttr: where(fn('lower', col('name')), null) },
- {
- default: `[booleanAttr] = (lower([name]) IS NULL)`,
- },
- );
- describe('where(leftOperand, operator, rightOperand)', () => {
- testSql(where(col('name'), Op.eq, fn('NOW')), {
- default: '[name] = NOW()',
- });
- // some dialects support having a filter inside aggregate functions:
- // https://github.com/sequelize/sequelize/issues/6666
- testSql(where(fn('sum', { id: 1 }), Op.eq, 1), {
- default: 'sum([id] = 1) = 1',
- });
- // some dialects support having a filter inside aggregate functions, but require casting:
- // https://github.com/sequelize/sequelize/issues/6666
- testSql(where(fn('sum', cast({ id: 1 }, 'int')), Op.eq, 1), {
- default: 'sum(CAST(([id] = 1) AS INT)) = 1',
- });
- // comparing the output of `where` to `where`
- testSql(where(where(col('col'), Op.eq, '1'), Op.eq, where(col('col'), Op.eq, '2')), {
- default: `([col] = '1') = ([col] = '2')`,
- mssql: `([col] = N'1') = ([col] = N'2')`,
- });
- testSql(where(1, Op.eq, 2), {
- default: '1 = 2',
- });
- testSql(where(1, Op.eq, col('col')), {
- default: '1 = [col]',
- });
- testSql(where('string', Op.eq, col('col')), {
- default: `'string' = [col]`,
- mssql: `N'string' = [col]`,
- });
- testSql(where('a', Op.eq, 'b'), {
- default: `'a' = 'b'`,
- mssql: `N'a' = N'b'`,
- });
- it('does not allow string operators', () => {
- // @ts-expect-error -- testing that this errors
- expect(() => where(fn('SUM', col('hours')), '>', 0)).to.throw(
- 'where(left, operator, right) does not accept a string as the operator',
- );
- });
- testSql(where(fn('SUM', col('hours')), Op.gt, 0), {
- default: 'SUM([hours]) > 0',
- });
- testSql(where(fn('lower', col('name')), Op.ne, null), {
- default: 'lower([name]) IS NOT NULL',
- });
- // @ts-expect-error -- While these are supported for backwards compatibility, they are not documented. Users should use isNot
- testSql(where(fn('lower', col('name')), Op.not, null), {
- default: 'NOT (lower([name]) IS NULL)',
- });
- testSql(where(fn('lower', col('name')), Op.isNot, null), {
- default: 'lower([name]) IS NOT NULL',
- });
- testSql(where(col('hours'), Op.between, [0, 5]), {
- default: '[hours] BETWEEN 0 AND 5',
- });
- testSql(where(col('hours'), Op.notBetween, [0, 5]), {
- default: '[hours] NOT BETWEEN 0 AND 5',
- });
- testSql(where({ [Op.col]: 'hours' }, Op.notBetween, [0, 5]), {
- default: '[hours] NOT BETWEEN 0 AND 5',
- });
- testSql(where(cast({ [Op.col]: 'hours' }, 'integer'), Op.notBetween, [0, 5]), {
- default: 'CAST([hours] AS INTEGER) NOT BETWEEN 0 AND 5',
- });
- testSql(where(fn('SUM', { [Op.col]: 'hours' }), Op.notBetween, [0, 5]), {
- default: 'SUM([hours]) NOT BETWEEN 0 AND 5',
- });
- testSql(where(literal(`'hours'`), Op.eq, 'hours'), {
- default: `'hours' = 'hours'`,
- mssql: `'hours' = N'hours'`,
- });
- testSql(where(col('col'), Op.eq, { [Op.in]: [1, 2] }), {
- default: new Error('Could not guess type of value { [Symbol(in)]: [ 1, 2 ] }'),
- });
- });
- describe('where(leftOperand, whereAttributeHashValue)', () => {
- testSql(where(fn('lower', col('name')), null), {
- default: 'lower([name]) IS NULL',
- });
- testSql(where(cast(col('name'), 'int'), { [Op.eq]: 10 }), {
- default: 'CAST([name] AS INT) = 10',
- });
- testSql(where(literal('abc'), { [Op.eq]: 10 }), {
- default: 'abc = 10',
- });
- testSql(where(col('name'), { [Op.eq]: '123', [Op.not]: { [Op.eq]: '456' } }), {
- default: `[name] = '123' AND NOT ([name] = '456')`,
- mssql: `[name] = N'123' AND NOT ([name] = N'456')`,
- });
- testSql(where(col('name'), or({ [Op.eq]: '123', [Op.not]: { [Op.eq]: '456' } })), {
- default: `[name] = '123' OR NOT ([name] = '456')`,
- mssql: `[name] = N'123' OR NOT ([name] = N'456')`,
- });
- testSql(
- // Note: using `col()`, the following is not treated as a json.path.
- // (yes, it's inconsistant with regular attribute notation. attr could be a good replacement)
- where(col('attribute.path'), 10),
- {
- default: '[attribute].[path] = 10',
- },
- );
- testSql(
- // Note: using `col()`, the following is not treated as a nested.attribute.path.
- // (yes, it's inconsistant with regular attribute notation. attr could be a good replacement)
- where(col('$attribute.path$'), 10),
- {
- default: '[$attribute].[path$] = 10',
- },
- );
- testSql(where(col('col'), { [Op.and]: [1, 2] }), {
- default: '[col] = 1 AND [col] = 2',
- });
- if (dialectSupportsJsonOperations() && dialectSupportsJsonQuotedExtraction()) {
- testSql(where(col('col'), { jsonPath: 'value' }), {
- postgres: `"col"->'jsonPath' = '"value"'`,
- sqlite3: `json_extract(\`col\`,'$.jsonPath') = '"value"'`,
- mariadb: `json_compact(json_extract(\`col\`,'$.jsonPath')) = '"value"'`,
- mysql: `json_extract(\`col\`,'$.jsonPath') = CAST('"value"' AS JSON)`,
- });
- }
- });
- });
- });
- });
|